'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
= htmlspecialchars($loginError) ?>
Secret Shortcut: Ctrl + Alt + F / Ketik forensic
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;
?>
= date('Y-m-d', strtotime($ts)) ?> = $timeStr ?> |
= $lvl ?> |
= htmlspecialchars($l['channel'] ?? '-') ?> |
= htmlspecialchars($ctx['ip_address'] ?? '-') ?> |
= htmlspecialchars($ctx['user_nip'] ?? '-') ?> |
= htmlspecialchars($msgShort) ?>...
Detail
= htmlspecialchars($msg) ?>
Ringkas
= htmlspecialchars($msg) ?>
= htmlspecialchars($ctx['request_uri']) ?>
|
|
|
Data Log JSON Structure (Row #= $idx + 1 ?>)
= htmlspecialchars(json_encode($l, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) ?>
|
$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
= htmlspecialchars($actionMessage) ?>
Total Event
= number_format($stats['total']) ?>
= $filterDate ?>
Login Sukses
= number_format($stats['login_success']) ?>
Gagal: = $stats['login_failed'] ?>
Session Expired
= number_format($stats['session_expired']) ?>
Navigasi Terhalang
Error & Critical
= number_format($stats['errors']) ?>
Warning: = $stats['warnings'] ?>
RataΒ² Response
= $stats['avg_response_ms'] ?> ms
= $stats['performance_count'] ?> Request
Brute Force Locked
= count($lockedIPs) ?>
IP Terblokir
| IP Address |
Total Gagal |
Sisa Waktu Lockout |
Aksi Unblock |
| = htmlspecialchars($b['ip']) ?> |
= count($b['attempts'] ?? []) ?>x Percobaan Gagal |
|
|
Tanggal Log: = htmlspecialchars($filterDate) ?>
|
Menampilkan = $totalLogs > 0 ? $offset + 1 : 0 ?> - = min($offset + $perPage, $totalLogs) ?> dari = number_format($totalLogs) ?> baris (Halaman = $currentPage ?> dari = $totalPages ?>)
| Waktu |
Level |
Channel |
IP Address |
User NIP |
Pesan / Event |
Detail |
|
Tidak ada log ditemukan untuk kriteria filter ini.
|
$l): ?>
0): ?>
Menampilkan Halaman = $currentPage ?> dari = $totalPages ?> (Total = number_format($totalLogs) ?> 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 |
| = htmlspecialchars($bk['filename']) ?> |
= $bk['size_mb'] ?> MB |
= $bk['mtime'] ?> |
Baca Log ZIP
Safe Archive
|