'stim@rssa.id', 'name' => 'STIM RSSA Admin (Pengembangan SSO)', 'email' => 'stim@rssa.id', 'login_time' => date('Y-m-d H:i:s'), 'auth_type' => 'temporary_local', 'kc_server' => 'local', 'kc_realm' => 'simrs', ]; if (isset($forensic)) { $forensic->logAuth('login_success_temporary', [ 'user_nip' => 'stim@rssa.id', 'auth_type' => 'temporary_local', 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', ], 'INFO'); } header('Location: forensic_viewer.php'); exit; } // 2. Autentikasi Direct Keycloak SSO $baseServer = rtrim($kcServer, '/'); if (strpos($baseServer, '/protocol/openid-connect') !== false) { $tokenEndpoint = $baseServer; } elseif (strpos($baseServer, '/realms/') !== false) { $tokenEndpoint = $baseServer . '/protocol/openid-connect/token'; } else { $tokenEndpoint = $baseServer . '/realms/' . rawurlencode($kcRealm) . '/protocol/openid-connect/token'; } $postParams = [ 'grant_type' => 'password', 'client_id' => $kcClientId, 'username' => $username, 'password' => $password, ]; if (!empty($kcClientSecret)) { $postParams['client_secret'] = $kcClientSecret; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $tokenEndpoint); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postParams)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 8); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErr = curl_error($ch); curl_close($ch); if ($curlErr) { $loginError = 'Gagal terhubung ke Server Keycloak SSO (' . htmlspecialchars($tokenEndpoint) . '): ' . $curlErr; } else { $jsonData = json_decode($response, true); if ($httpCode === 200 && !empty($jsonData['access_token'])) { $tokenParts = explode('.', $jsonData['access_token']); $payload = []; if (isset($tokenParts[1])) { $decoded = base64_decode(strtr($tokenParts[1], '-_', '+/')); $payload = json_decode($decoded, true) ?: []; } $_SESSION['forensic_user'] = [ 'username' => $payload['preferred_username'] ?? $username, 'name' => $payload['name'] ?? ($payload['preferred_username'] ?? $username), 'email' => $payload['email'] ?? '', 'login_time' => date('Y-m-d H:i:s'), 'access_token' => $jsonData['access_token'], 'auth_type' => 'keycloak_sso', 'kc_server' => $kcServer, 'kc_realm' => $kcRealm, ]; if (isset($forensic)) { $forensic->logAuth('login_success', [ 'user_nip' => $username, 'sso_type' => 'keycloak_direct', 'realm' => $kcRealm, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', ], 'INFO'); } header('Location: forensic_viewer.php'); exit; } else { $errMsg = $jsonData['error_description'] ?? ($jsonData['error'] ?? 'Autentikasi Gagal (HTTP ' . $httpCode . ')'); $loginError = 'Login Error: Username / Password tidak sesuai atau ' . $errMsg; if (isset($forensic)) { $forensic->logAuth('login_failed', [ 'user_nip' => $username, 'sso_type' => 'keycloak_direct', 'http_code' => $httpCode, 'error' => $errMsg, ], 'WARNING'); } } } } } // Access Check: forensic_viewer.php WAJIB Login terlebih dahulu! (Direct access dilarang tanpa sesi aktif) $isSsoLoggedIn = !empty($_SESSION['forensic_user']) || !empty($_SESSION['SES_REG']); // Form login Keycloak SSO & Local (Tampil otomatis jika pengguna belum terautentikasi) if (!$isSsoLoggedIn): ?> Login Keycloak SSO β€” SIMRS Forensic Inspector

SIMRS Forensic Inspector

Masuk menggunakan Keycloak SSO atau Akun Login Sementara

Secret Shortcut: Ctrl + Alt + F / Ketik forensic
Kembali ke SIMRS Utama
run(); $actionMessage = 'Job retention & backup berhasil dijalankan. (Forensic: ' . ($res['forensic'] ?? 0) . ' files, Activity: ' . ($res['activity'] ?? 0) . ' files archived)'; } // Action: Clear Brute Force Lockout if (isset($_POST['action']) && $_POST['action'] === 'clear_bf' && !empty($_POST['target_ip'])) { if (isset($forensic)) { $forensic->clearBruteForce($_POST['target_ip']); $actionMessage = 'Lockout IP ' . htmlspecialchars($_POST['target_ip']) . ' berhasil dibersihkan.'; } } // Filters $filterDate = $_GET['date'] ?? ''; $filterChannel = $_GET['channel'] ?? 'all'; $filterLevel = $_GET['level'] ?? 'all'; $filterCorrelation = $_GET['correlation'] ?? ''; $filterSearch = $_GET['search'] ?? ''; $filterTimeStart = $_GET['time_start'] ?? ''; $filterTimeEnd = $_GET['time_end'] ?? ''; $filterSource = $_GET['source'] ?? 'auto'; // Sinkronisasi Tanggal Otomatis jika Memilih File Berkas Arsip ZIP Spesifik $isSpecificZip = (strpos($filterSource, '.zip') !== false); if ($isSpecificZip) { if (preg_match('/(20\d{2}-\d{2})/', $filterSource, $zipYmMatch)) { $zipYm = $zipYmMatch[1]; if (!isset($_GET['date']) || strpos($_GET['date'], $zipYm) === false) { $filterDate = $zipYm . '-01'; } } } if (empty($filterDate)) { $filterDate = date('Y-m-d'); } // Scan Backup Archives terlebih dahulu $backupDir = _DOCROOT_ . 'logs/backup/'; $backups = []; if (is_dir($backupDir)) { foreach (glob($backupDir . '*.zip') as $zipFile) { $backups[] = [ 'filename' => basename($zipFile), 'size_mb' => round(filesize($zipFile) / 1024 / 1024, 2), 'mtime' => date('Y-m-d H:i:s', filemtime($zipFile)), ]; } // Urutkan arsip ZIP terbaru di atas (DESC by filename & date) usort($backups, function ($a, $b) { return strcmp($b['filename'], $a['filename']); }); } // Helper: Scan log files for selected date across all log directories $logsRoot = _DOCROOT_ . 'logs/'; $year = date('Y', strtotime($filterDate)); $month = date('m', strtotime($filterDate)); $day = date('d', strtotime($filterDate)); $dmY = date('d-m-Y', strtotime($filterDate)); $logs = []; $stats = [ 'total' => 0, 'login_success' => 0, 'login_failed' => 0, 'session_expired' => 0, 'errors' => 0, 'warnings' => 0, 'performance_count' => 0, 'avg_response_ms' => 0, ]; $totalRespMs = 0; $extractLogMetaData = function(&$entry, $rawLine) { if (!isset($entry['context']) || !is_array($entry['context'])) { $entry['context'] = []; } $msg = $entry['message'] ?? $rawLine; $searchTarget = $rawLine . ' ' . $msg . ' ' . json_encode($entry['context']); // 1. EXTRACTION: IP ADDRESS $extractedIp = $entry['context']['ip_address'] ?? ($entry['context']['ip'] ?? ($entry['context']['remote_addr'] ?? ($entry['ip_address'] ?? ($entry['ip'] ?? '')))); if (empty($extractedIp) || $extractedIp === '-') { if (preg_match_all('/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/', $searchTarget, $ipMatches)) { $foundIps = array_unique($ipMatches[0]); $bestIp = ''; foreach ($foundIps as $ip) { if ($ip !== '127.0.0.1' && $ip !== '0.0.0.0') { $bestIp = $ip; break; } } $extractedIp = !empty($bestIp) ? $bestIp : reset($foundIps); } } if (!empty($extractedIp)) { $entry['context']['ip_address'] = $extractedIp; } // 2. EXTRACTION: USER NIP / USERNAME / PETUGAS $extractedNip = $entry['context']['user_nip'] ?? ($entry['context']['user'] ?? ($entry['context']['nip'] ?? ($entry['context']['username'] ?? ($entry['user_nip'] ?? ($entry['nip'] ?? ''))))); if (empty($extractedNip) || $extractedNip === '-') { if (preg_match('/\["([^"]+)"\s*,\s*"([^"]*http[^"]*)"/i', $searchTarget, $mNameUrl)) { $extractedNip = trim($mNameUrl[1]); if (empty($entry['context']['request_uri'])) { $entry['context']['request_uri'] = trim($mNameUrl[2]); } } elseif (preg_match('/(?:NIP|User|Petugas|Akun|oleh)[:\s]+([A-Za-z0-9_\-\.\,\@\s]{3,40})/i', $searchTarget, $mNip)) { $extractedNip = trim($mNip[1]); } } if (!empty($extractedNip)) { $entry['context']['user_nip'] = $extractedNip; } // 3. EXTRACTION: REQUEST URI / URL $extractedUri = $entry['context']['request_uri'] ?? ($entry['context']['uri'] ?? ($entry['context']['url'] ?? ($entry['request_uri'] ?? ($entry['uri'] ?? '')))); if (empty($extractedUri) || $extractedUri === '-') { if (preg_match('/https?:\/\/[^\s"\'\]]+/', $searchTarget, $mUrl)) { $extractedUri = $mUrl[0]; } elseif (preg_match('/\s(\/[a-zA-Z0-9_\-\/\.]+\.php[^\s"\'\]]*)/', $searchTarget, $mUri)) { $extractedUri = $mUri[1]; } } if (!empty($extractedUri)) { $entry['context']['request_uri'] = $extractedUri; } }; $processLogLine = function ($line, $sourceChannel = 'access') use (&$logs, &$stats, &$totalRespMs, &$respCount, $filterLevel, $filterTimeStart, $filterTimeEnd, $filterCorrelation, $filterSearch, $filterChannel, $extractLogMetaData, $filterDate) { $line = trim($line); if (empty($line)) return; static $lineSeq = 0; $lineSeq++; $entry = json_decode($line, true); if (!$entry || !is_array($entry)) { // Parsing log non-JSON (Access, Activity, Retention log) $defaultTs = !empty($filterDate) ? $filterDate . ' 00:00:00' : date('Y-m-d H:i:s'); $timestamp = ''; if (preg_match('/\[(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]/', $line, $m)) { $timestamp = str_replace('T', ' ', $m[1]); } elseif (preg_match('/(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)/', $line, $m)) { $timestamp = str_replace('T', ' ', $m[1]); } elseif (preg_match('/(\d{2}-\d{2}-\d{4}[ T]\d{2}:\d{2}:\d{2})/', $line, $m)) { $timestamp = date('Y-m-d H:i:s', strtotime($m[1])); } elseif (preg_match('/(\d{4}\/\d{2}\/\d{2}[ T]\d{2}:\d{2}:\d{2})/', $line, $m)) { $timestamp = date('Y-m-d H:i:s', strtotime(str_replace('/', '-', $m[1]))); } if (empty($timestamp)) { $timestamp = $defaultTs; } else { if (preg_match('/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/', $timestamp, $tmClean)) { $timestamp = $tmClean[1]; } } $entry = [ 'timestamp' => $timestamp, 'channel' => $sourceChannel ?: 'access', 'level' => 'INFO', 'message' => $line, 'correlation_id' => '-', 'context' => [ 'raw_log' => $line ] ]; } else { if (empty($entry['timestamp'])) { $tsVal = $entry['created_at'] ?? ($entry['date'] ?? ($entry['time'] ?? '')); if (!empty($tsVal)) { $entry['timestamp'] = date('Y-m-d H:i:s', strtotime($tsVal)); } else { if (preg_match('/(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2})/', $line, $m)) { $entry['timestamp'] = str_replace('T', ' ', $m[1]); } else { $entry['timestamp'] = !empty($filterDate) ? $filterDate . ' 00:00:00' : date('Y-m-d H:i:s'); } } } } $entry['_seq'] = $lineSeq; // Ekstraksi metadata pintar (IP Address, User NIP, URI) $extractLogMetaData($entry, $line); $ch = $entry['channel'] ?? $sourceChannel; if ($filterChannel !== 'all' && strtolower($ch) !== strtolower($filterChannel)) { return; } $lvl = strtoupper($entry['level'] ?? 'INFO'); if ($filterLevel !== 'all' && $lvl !== strtoupper($filterLevel)) { return; } if ($filterTimeStart !== '' || $filterTimeEnd !== '') { $entryTime = ''; if (isset($entry['timestamp'])) { if (preg_match('/[T ](\d{2}:\d{2})/', $entry['timestamp'], $timeMatch)) { $entryTime = $timeMatch[1]; } else { $entryTime = date('H:i', strtotime($entry['timestamp'])); } } if ($filterTimeStart !== '' && $entryTime !== '' && $entryTime < $filterTimeStart) { return; } if ($filterTimeEnd !== '' && $entryTime !== '' && $entryTime > $filterTimeEnd) { return; } } if ($filterCorrelation !== '' && ($entry['correlation_id'] ?? '') !== $filterCorrelation) { return; } if ($filterSearch !== '') { $searchTarget = ($entry['message'] ?? '') . ' ' . ($entry['channel'] ?? '') . ' ' . ($entry['level'] ?? '') . ' ' . ($entry['correlation_id'] ?? '') . ' ' . ($entry['context']['ip_address'] ?? '') . ' ' . ($entry['context']['user_nip'] ?? '') . ' ' . ($entry['context']['request_uri'] ?? '') . ' ' . json_encode($entry['context'] ?? [], JSON_UNESCAPED_SLASHES); if (stripos($searchTarget, $filterSearch) === false) { return; } } $stats['total']++; if ($lvl === 'ERROR' || $lvl === 'CRITICAL') $stats['errors']++; if ($lvl === 'WARNING') $stats['warnings']++; $msg = $entry['message'] ?? ''; if (stripos($msg, 'login_success') !== false || stripos($msg, 'login ok') !== false || stripos($msg, 'login berhasil') !== false) $stats['login_success']++; if (stripos($msg, 'login_failed') !== false || stripos($msg, 'gagal login') !== false) $stats['login_failed']++; if (stripos($msg, 'session_expired') !== false || stripos($msg, 'session habis') !== false) $stats['session_expired']++; if (strtolower($ch) === 'performance') { $stats['performance_count']++; if (isset($entry['context']['total_duration_ms'])) { $totalRespMs += $entry['context']['total_duration_ms']; $respCount++; } } $logs[] = $entry; }; // 1. Baca Forensic Logs (/app/logs/forensic/YYYY/MM/*.log) if ($filterSource === 'auto' || $filterSource === 'active') { $forensicDir = $logsRoot . 'forensic/' . $year . '/' . $month . '/'; if (is_dir($forensicDir)) { foreach (glob($forensicDir . '*.log') as $fPath) { $fName = basename($fPath); if (strpos($fName, $filterDate) !== false) { $chName = explode('_', $fName)[0] ?? 'forensic'; $handle = @fopen($fPath, 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { $processLogLine($line, $chName); } fclose($handle); } } } } } // 2. Baca Activity Logs (/app/logs/activity/YYYY/MM/DD-MM-YYYY.log) if ($filterSource === 'auto' || $filterSource === 'active') { $activityPath = $logsRoot . 'activity/' . $year . '/' . $month . '/' . $dmY . '.log'; if (file_exists($activityPath)) { $handle = @fopen($activityPath, 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { $processLogLine($line, 'activity'); } fclose($handle); } } } // 3. Baca Access Logs (/app/logs/access/YYYY/MM/DD/*.log) if ($filterSource === 'auto' || $filterSource === 'active') { $accessDir = $logsRoot . 'access/' . $year . '/' . $month . '/' . $day . '/'; if (is_dir($accessDir)) { foreach (glob($accessDir . '*.log') as $aPath) { $aName = basename($aPath, '.log'); $handle = @fopen($aPath, 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { $processLogLine($line, 'access'); } fclose($handle); } } } } // 4. Baca System Retention Log (/app/logs/retention.log) if ($filterSource === 'auto' || $filterSource === 'active') { $retentionPath = $logsRoot . 'retention.log'; if (file_exists($retentionPath)) { $handle = @fopen($retentionPath, 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { if (empty($filterDate) || strpos($line, $filterDate) !== false) { $processLogLine($line, 'system'); } } fclose($handle); } } } // 5. Baca ZIP Archives (/app/logs/backup/*.zip) $zipFilesToProcess = []; if (is_dir($backupDir)) { if (strpos($filterSource, '.zip') !== false && file_exists($backupDir . $filterSource)) { $zipFilesToProcess[] = $backupDir . $filterSource; } elseif ($filterSource === 'all_zip' || ($filterSource === 'auto' && empty($logs))) { foreach (glob($backupDir . '*.zip') as $zFile) { if ($filterSource === 'auto' && !empty($filterDate)) { $ymMonth = date('Y-m', strtotime($filterDate)); if (strpos(basename($zFile), $ymMonth) !== false) { $zipFilesToProcess[] = $zFile; } } else { $zipFilesToProcess[] = $zFile; } } } } if (!empty($zipFilesToProcess) && class_exists('ZipArchive')) { $zip = new ZipArchive(); foreach ($zipFilesToProcess as $zPath) { if ($zip->open($zPath) === true) { for ($i = 0; $i < $zip->numFiles; $i++) { $entryName = $zip->getNameIndex($i); if (!empty($filterDate) && $filterSource === 'auto') { if (strpos($entryName, $filterDate) === false && strpos($entryName, $dmY) === false && strpos($entryName, '/' . $day . '/') === false) { continue; } } $stream = $zip->getStream($entryName); if ($stream) { $ch = 'archive'; if (strpos($entryName, 'session') !== false) $ch = 'session'; elseif (strpos($entryName, 'auth') !== false) $ch = 'auth'; elseif (strpos($entryName, 'error') !== false) $ch = 'error'; elseif (strpos($entryName, 'performance') !== false) $ch = 'performance'; elseif (strpos($entryName, 'security') !== false) $ch = 'security'; elseif (strpos($entryName, 'access') !== false) $ch = 'access'; elseif (strpos($entryName, 'activity') !== false) $ch = 'activity'; while (($line = fgets($stream)) !== false) { $processLogLine($line, $ch); } fclose($stream); } } $zip->close(); } } } if ($respCount > 0) { $stats['avg_response_ms'] = round($totalRespMs / $respCount, 2); } // Sort logs strictly descending by timestamp (newest first), with sequence index as secondary tie-breaker usort($logs, function ($a, $b) { $tsA = strtotime($a['timestamp'] ?? '1970-01-01'); $tsB = strtotime($b['timestamp'] ?? '1970-01-01'); if ($tsA !== $tsB) { return $tsB <=> $tsA; // Descending: newest log entry at the top } return ($b['_seq'] ?? 0) <=> ($a['_seq'] ?? 0); // Newest line in file on top }); // Pagination Configuration & Lazy Load / Infinite Scroll (Limit 100 per page default) $perPage = isset($_GET['limit']) ? (int)$_GET['limit'] : 100; if (!in_array($perPage, [50, 100, 250, 500, 1000])) { $perPage = 100; } $totalLogs = count($logs); $totalPages = (int)ceil($totalLogs / $perPage); if ($totalPages < 1) $totalPages = 1; $currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1; if ($currentPage < 1) $currentPage = 1; if ($currentPage > $totalPages) $currentPage = $totalPages; $offset = ($currentPage - 1) * $perPage; $pagedLogs = array_slice($logs, $offset, $perPage); // Helper function to render a single log row cleanly $renderLogRow = function ($l, $idx) { $lvl = $l['level'] ?? 'INFO'; $ts = $l['timestamp'] ?? ''; $timeStr = !empty($ts) ? date('H:i:s', strtotime($ts)) : '-'; $ctx = $l['context'] ?? []; $rowId = 'detail_' . $idx; $msg = $l['message'] ?? ''; $isLong = mb_strlen($msg) > 130; $msgShort = $isLong ? mb_substr($msg, 0, 130) : $msg; $msgId = 'msg_' . $idx; ?>
... Detail
Data Log JSON Structure (Row #)
$l) { $renderLogRow($l, $offset + $i); } $html = ob_get_clean(); header('Content-Type: application/json'); echo json_encode([ 'html' => $html, 'currentPage' => $currentPage, 'totalPages' => $totalPages, 'hasNext' => $currentPage < $totalPages, 'nextPage' => $currentPage + 1, 'totalLogs' => $totalLogs, 'countLoaded' => count($pagedLogs), ]); exit; } // Scan Brute Force cache $bfDir = $logsRoot . 'forensic/cache/'; $lockedIPs = []; if (is_dir($bfDir)) { foreach (glob($bfDir . 'bf_*.json') as $bfFile) { $bfData = json_decode(@file_get_contents($bfFile), true); if ($bfData && isset($bfData['lockout_until']) && $bfData['lockout_until'] > time()) { $lockedIPs[] = $bfData; } } } // Export CSV / JSON action if (isset($_GET['export'])) { if ($_GET['export'] === 'json') { header('Content-Type: application/json'); header('Content-Disposition: attachment; filename="forensic_log_' . $filterDate . '.json"'); echo json_encode($logs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); exit; } elseif ($_GET['export'] === 'csv') { header('Content-Type: text/csv'); header('Content-Disposition: attachment; filename="forensic_log_' . $filterDate . '.csv"'); $out = fopen('php://output', 'w'); fputcsv($out, ['Timestamp', 'Correlation ID', 'Channel', 'Level', 'Message', 'IP', 'NIP', 'URI']); foreach ($logs as $l) { fputcsv($out, [ $l['timestamp'] ?? '', $l['correlation_id'] ?? '', $l['channel'] ?? '', $l['level'] ?? '', $l['message'] ?? '', $l['context']['ip_address'] ?? '', $l['context']['user_nip'] ?? '', $l['context']['request_uri'] ?? '', ]); } fclose($out); exit; } } $currentUser = $_SESSION['forensic_user']['username'] ?? ($_SESSION['SES_REG'] ?? 'Admin'); ?> SIMRS Forensic Inspector
Total Event
Login Sukses
Gagal:
Session Expired
Navigasi Terhalang
Error & Critical
Warning:
RataΒ² Response
ms
Request
Brute Force Locked
IP Terblokir
Peringatan Keamanan: Perangkat / IP Terblokir (Brute Force Detected)
IP Address Total Gagal Sisa Waktu Lockout Aksi Unblock
x Percobaan Gagal
-
Tanggal Log: | Menampilkan 0 ? $offset + 1 : 0 ?> - dari baris (Halaman dari )
$v): ?>
Export CSV Export JSON
$l): ?>
Waktu Level Channel IP Address User NIP Pesan / Event Detail
Tidak ada log ditemukan untuk kriteria filter ini.
0): ?>
Menampilkan Halaman dari (Total log)
1): ?>
Berkas log berusia >30 hari diarsipkan secara kompresi per bulan.
Belum ada file arsip log zip di folder logs/backup/. File log berusia >30 hari akan diarsipkan otomatis.
Nama Berkas Arsip Ukuran File Waktu Pembuatan Aksi & Status
MB Baca Log ZIP Safe Archive