'success', 'text' => 'Laporan berhasil dianalisis!'];
} else {
$message = ['type' => 'error', 'text' => $result['error']];
}
}
// ── Handle: Delete history entry ───────────────────────────
if ($action === 'delete' && !empty($_GET['id'])) {
deleteHistory($_GET['id']);
header('Location: index.php?deleted=1');
exit;
}
// ── Handle: View historical report ─────────────────────────
if ($action === 'view' && !empty($_GET['id'])) {
$report = getHistoryItem($_GET['id']);
}
// ── Load history ───────────────────────────────────────────
$history = loadHistory();
// ============================================================
// FUNCTIONS
// ============================================================
function handleUpload($file) {
if ($file['error'] !== UPLOAD_ERR_OK)
return ['success' => false, 'error' => 'Upload gagal. Coba lagi.'];
if ($file['size'] > MAX_FILE_SIZE)
return ['success' => false, 'error' => 'File terlalu besar (max 10MB).'];
if (mime_content_type($file['tmp_name']) !== 'application/pdf')
return ['success' => false, 'error' => 'File harus berformat PDF.'];
// Extract text
$text = extractPdfText($file['tmp_name']);
if (!$text)
return ['success' => false, 'error' => 'Gagal membaca isi PDF. Pastikan PDF berisi teks (bukan scan gambar).'];
// AI Analysis
$analysis = analyzeWithAI($text);
if (!$analysis)
return ['success' => false, 'error' => 'Gagal menghubungi AI. Periksa API key.'];
// Save
$report = [
'id' => uniqid(),
'filename' => htmlspecialchars($file['name']),
'uploaded' => date('Y-m-d H:i:s'),
'analysis' => $analysis,
'raw_text' => substr($text, 0, 3000), // simpan preview
];
saveHistory($report);
return ['success' => true, 'report' => $report];
}
function extractPdfText($filepath) {
global $needsComposer;
if ($needsComposer) return null;
try {
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile($filepath);
$text = $pdf->getText();
return strlen(trim($text)) > 100 ? $text : null;
} catch (Exception $e) {
return null;
}
}
function analyzeWithAI($text) {
$prompt = buildPrompt($text);
// Try Claude first
$result = callClaude($prompt);
if ($result) return $result;
// Fallback to Groq
$result = callGroq($prompt);
return $result;
}
function buildPrompt($text) {
return "You are a network security analyst. Analyze the following FortiGate report and return ONLY a valid JSON object (no markdown, no extra text) with this exact structure:
{
\"device\": \"device name\",
\"period\": \"report period\",
\"total_traffic_gb\": 0,
\"total_sessions\": 0,
\"total_threats\": 0,
\"attacks\": 0,
\"threat_items\": [
{\"name\": \"threat name\", \"level\": \"High/Medium/Low\", \"score\": 0, \"percent\": \"0%\"}
],
\"top_apps\": [
{\"name\": \"app name\", \"traffic_gb\": 0, \"percent\": \"0%\"}
],
\"web_categories\": [
{\"name\": \"category\", \"percent\": \"0%\"}
],
\"vpn_threats\": [\"app1\", \"app2\"],
\"suspicious_domains\": [\"domain1\"],
\"top_users\": [
{\"name\": \"username\", \"visits\": 0}
],
\"vpn_tunnels\": [
{\"name\": \"tunnel\", \"sent_gb\": 0, \"recv_gb\": 0}
],
\"traffic_by_country\": [
{\"country\": \"country name\", \"traffic_gb\": 0, \"percent\": \"0%\"}
],
\"admin_sessions\": [
{\"user\": \"admin\", \"interface\": \"https\", \"sessions\": 0, \"changes\": 0}
],
\"failed_admin_logins\": 0,
\"virus_detected\": false,
\"recommendations\": [
{
\"level\": \"high/medium/low\",
\"title_id\": \"judul dalam Bahasa Indonesia\",
\"title_en\": \"title in English\",
\"desc_id\": \"penjelasan singkat dan jelas dalam Bahasa Indonesia\",
\"desc_en\": \"concise explanation in English\"
}
],
\"summary_id\": \"Ringkasan 2-3 kalimat kondisi keamanan jaringan hari ini dalam Bahasa Indonesia.\",
\"summary_en\": \"2-3 sentence summary of today's network security status in English.\"
}
IMPORTANT: The recommendations array MUST include both Indonesian (title_id, desc_id) and English (title_en, desc_en) fields for every item. Do not use the old 'title' or 'desc' fields.
REPORT DATA:
" . substr($text, 0, 8000);
}
function callClaude($prompt) {
$payload = json_encode([
'model' => 'claude-haiku-4-5',
'max_tokens' => 2000,
'messages' => [['role' => 'user', 'content' => $prompt]],
]);
$response = httpPost('https://api.anthropic.com/v1/messages', $payload, [
'x-api-key: ' . CLAUDE_API_KEY,
'anthropic-version: 2023-06-01',
'content-type: application/json',
]);
if (!$response) return null;
$data = json_decode($response, true);
$text = $data['content'][0]['text'] ?? null;
return $text ? parseJsonResponse($text) : null;
}
function callGroq($prompt) {
$payload = json_encode([
'model' => 'llama-3.3-70b-versatile',
'messages' => [
['role' => 'system', 'content' => 'Kamu analis keamanan jaringan. Selalu balas HANYA dengan JSON valid.'],
['role' => 'user', 'content' => $prompt],
],
'max_tokens' => 2000,
'temperature' => 0.1,
]);
$response = httpPost('https://api.groq.com/openai/v1/chat/completions', $payload, [
'Authorization: Bearer ' . GROQ_API_KEY,
'Content-Type: application/json',
]);
if (!$response) return null;
$data = json_decode($response, true);
$text = $data['choices'][0]['message']['content'] ?? null;
return $text ? parseJsonResponse($text) : null;
}
function parseJsonResponse($text) {
// Strip markdown fences if any
$text = preg_replace('/^```(?:json)?\s*/m', '', $text);
$text = preg_replace('/\s*```$/m', '', $text);
$text = trim($text);
$data = json_decode($text, true);
return (json_last_error() === JSON_ERROR_NONE && is_array($data)) ? $data : null;
}
function httpPost($url, $payload, $headers) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 60,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ($response && $httpCode === 200) ? $response : null;
}
function loadHistory() {
if (!file_exists(HISTORY_FILE)) return [];
$data = json_decode(file_get_contents(HISTORY_FILE), true);
return is_array($data) ? array_reverse($data) : [];
}
function saveHistory($report) {
$history = file_exists(HISTORY_FILE)
? json_decode(file_get_contents(HISTORY_FILE), true) : [];
if (!is_array($history)) $history = [];
$history[] = $report;
// Keep max 90 entries
if (count($history) > 90) $history = array_slice($history, -90);
file_put_contents(HISTORY_FILE, json_encode($history, JSON_PRETTY_PRINT));
}
function getHistoryItem($id) {
$history = loadHistory();
foreach ($history as $item) {
if ($item['id'] === $id) return $item;
}
return null;
}
function deleteHistory($id) {
if (!file_exists(HISTORY_FILE)) return;
$history = json_decode(file_get_contents(HISTORY_FILE), true);
if (!is_array($history)) return;
$history = array_values(array_filter($history, fn($h) => $h['id'] !== $id));
file_put_contents(HISTORY_FILE, json_encode($history, JSON_PRETTY_PRINT));
}
function levelBadge($level) {
$map = ['High' => 'danger', 'Medium' => 'warning', 'Low' => 'info'];
$cls = $map[$level] ?? 'secondary';
return "{$level}";
}
function recoBadge($level) {
$map = ['high' => 'danger', 'medium' => 'warning', 'low' => 'info'];
$cls = $map[strtolower($level)] ?? 'secondary';
$icons = ['high' => '🔴', 'medium' => '🟡', 'low' => '🔵'];
$icon = $icons[strtolower($level)] ?? '⚪';
return "{$icon} " . ucfirst($level) . "";
}
function fmtNum($n) {
return number_format((float)$n, 0, ',', '.');
}
?>
FortiGate Report Dashboard
Menganalisis laporan dengan AI...
Setup diperlukan
Install library pdfparser via Composer di cPanel sebelum menggunakan aplikasi ini:
- Login ke cPanel → Terminal
- Masuk ke folder aplikasi ini:
cd = htmlspecialchars(__DIR__) ?>
- Jalankan:
composer require smalot/pdfparser
- Refresh halaman ini
Jika Composer belum tersedia: curl -sS https://getcomposer.org/installer | php
= htmlspecialchars($message['text']) ?>
Pilih laporan dari riwayat di sebelah kiri
= htmlspecialchars($a['device'] ?? 'Unknown Device') ?>
= htmlspecialchars($a['period'] ?? '') ?>
·
= htmlspecialchars($report['filename']) ?>
·
Upload: = htmlspecialchars($report['uploaded']) ?>
Upload Baru
ID
= htmlspecialchars($a['summary_id'] ?? $a['summary'] ?? '') ?>
EN
= htmlspecialchars($a['summary_en']) ?>
'Total Traffic','value'=>($a['total_traffic_gb']??0).' GB','icon'=>'bi-hdd-network','bg'=>'#e8f4fd','color'=>'#0d6efd'],
['label'=>'Total Sesi','value'=>fmtNum($a['total_sessions']??0),'icon'=>'bi-diagram-3','bg'=>'#e8f5e9','color'=>'#198754'],
['label'=>'Total Ancaman','value'=>fmtNum($a['total_threats']??0),'icon'=>'bi-shield-exclamation','bg'=>'#fff3cd','color'=>'#fd7e14'],
['label'=>'Serangan URL','value'=>fmtNum($a['attacks']??0),'icon'=>'bi-bug','bg'=>'#fdecea','color'=>'#dc3545'],
];
foreach ($stats as $s): ?>
= $s['label'] ?>
= htmlspecialchars($s['value']) ?>
= recoBadge($r['level'] ?? 'low') ?>
ID
= htmlspecialchars($title_id) ?>
= htmlspecialchars($desc_id) ?>
EN
= htmlspecialchars($title_en) ?>
= htmlspecialchars($desc_en) ?>
| Ancaman | Level | % |
| = htmlspecialchars($t['name']??'') ?> |
= levelBadge($t['level']??'Low') ?> |
= htmlspecialchars($t['percent']??'') ?>
|
| Aplikasi | Traffic (GB) | % |
| = htmlspecialchars($app['name']??'') ?> |
= htmlspecialchars($app['traffic_gb']??0) ?> |
= htmlspecialchars($app['percent']??'') ?>
|
= htmlspecialchars($v) ?>
Aplikasi di atas terdeteksi aktif dan berisiko bypass kebijakan jaringan.
= htmlspecialchars($d) ?>
| = htmlspecialchars($wc['name']??'') ?> |
|
= htmlspecialchars($wc['percent']??'') ?>
= htmlspecialchars($wc['percent']??'') ?>
|
| # | User | Kunjungan |
$u): ?>
| = $i+1 ?> |
= htmlspecialchars($u['name']??'') ?> |
= fmtNum($u['visits']??0) ?> |
| Tunnel | Kirim (GB) | Terima (GB) |
| = htmlspecialchars($vt['name']??'') ?> |
= htmlspecialchars($vt['sent_gb']??0) ?> |
= htmlspecialchars($vt['recv_gb']??0) ?> |
| Negara | Traffic (GB) | % |
| = htmlspecialchars($c['country']??'') ?> |
= htmlspecialchars($c['traffic_gb']??0) ?> |
= htmlspecialchars($c['percent']??'') ?>
|
Tidak ada login gagal
= (int)$a['failed_admin_logins'] ?> login gagal
Tidak ada virus
Virus terdeteksi!
| User | Interface | Sesi | Perubahan Config |
| = htmlspecialchars($as['user']??'') ?> |
= htmlspecialchars($as['interface']??'') ?> |
= (int)($as['sessions']??0) ?> |
= (int)($as['changes']??0) ?> |