<?php
session_start();

error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('memory_limit', '512M');
set_time_limit(300);

/*
|--------------------------------------------------------------------------
| SECURITY AUDIT SCANNER
|--------------------------------------------------------------------------
| Read-only. Tidak menghapus file.
| Tujuan: cek bug keamanan per file dan kasih rekomendasi perbaikan.
|--------------------------------------------------------------------------
*/



$root = realpath(__DIR__ . '/../');

if (!$root || !is_dir($root)) {
    exit('Root website tidak ditemukan.');
}

function h($v)
{
    return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
}

function rel_path($path, $root)
{
    return ltrim(str_replace($root, '', $path), DIRECTORY_SEPARATOR);
}

function risk_score($level)
{
    if ($level === 'TINGGI') return 3;
    if ($level === 'SEDANG') return 2;
    return 1;
}

function add_issue(&$issues, $level, $file, $line, $title, $detail, $fix, $snippet = '')
{
    $issues[] = [
        'level' => $level,
        'score' => risk_score($level),
        'file' => $file,
        'line' => $line,
        'title' => $title,
        'detail' => $detail,
        'fix' => $fix,
        'snippet' => trim($snippet),
    ];
}

function is_excluded_path($relative)
{
    $relative = str_replace('\\', '/', $relative);

    $excludes = [
        '.git/',
        'node_modules/',
        'vendor/',
        'cache/',
        'tmp/',
        'logs/',
        'storage/cache/',
        'uvgaming/security_audit.php',
    ];

    foreach ($excludes as $ex) {
        if (stripos($relative, $ex) !== false) {
            return true;
        }
    }

    return false;
}

function file_ext($path)
{
    $base = basename($path);

    if ($base === '.htaccess') return 'htaccess';
    if ($base === '.user.ini') return 'ini';
    if ($base === '.env') return 'env';

    return strtolower(pathinfo($path, PATHINFO_EXTENSION));
}

$issues = [];
$totalFiles = 0;
$totalPhpFiles = 0;
$maxReadSize = 2 * 1024 * 1024;

$backupExt = ['zip', 'rar', '7z', 'sql', 'bak', 'old', 'backup', 'tar', 'gz', 'tgz'];
$phpExt = ['php', 'php3', 'php4', 'php5', 'phtml', 'phar', 'inc'];

$uploadDirs = [
    'asset',
    'assets',
    'upload',
    'uploads',
    'images',
    'img',
    'asset/images',
    'asset/upload',
    'assets/images',
    'assets/upload',
];

/*
|--------------------------------------------------------------------------
| Cek .htaccess proteksi folder upload/asset
|--------------------------------------------------------------------------
*/
foreach ($uploadDirs as $dir) {
    $dirPath = $root . '/' . $dir;

    if (!is_dir($dirPath)) {
        continue;
    }

    $ht = $dirPath . '/.htaccess';

    if (!is_file($ht)) {
        add_issue(
            $issues,
            'TINGGI',
            $dir . '/.htaccess',
            0,
            'Folder upload/asset belum punya proteksi .htaccess',
            'Folder ini bisa menjadi tempat webshell jika PHP bisa dieksekusi dari sini.',
            'Buat .htaccess di folder ini dan blok file php, phtml, phar.',
            ''
        );
        continue;
    }

    $htContent = @file_get_contents($ht);

    if (
        stripos($htContent, 'FilesMatch') === false ||
        stripos($htContent, 'php') === false ||
        stripos($htContent, 'denied') === false
    ) {
        add_issue(
            $issues,
            'TINGGI',
            $dir . '/.htaccess',
            1,
            'Proteksi .htaccess folder upload/asset masih lemah',
            'File .htaccess ada, tapi belum jelas memblokir eksekusi PHP.',
            'Isi .htaccess dengan FilesMatch untuk deny php, phtml, phar.',
            substr((string)$htContent, 0, 300)
        );
    }
}

/*
|--------------------------------------------------------------------------
| Scan semua file
|--------------------------------------------------------------------------
*/
try {
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($iterator as $file) {
        if (!$file->isFile()) {
            continue;
        }

        $path = $file->getPathname();
        $relative = rel_path($path, $root);
        $relativeUnix = str_replace('\\', '/', $relative);

        if (is_excluded_path($relativeUnix)) {
            continue;
        }

        $totalFiles++;

        $ext = file_ext($path);
        $base = strtolower($file->getBasename());
        $size = $file->getSize();
        $mtime = $file->getMTime();

        /*
        |--------------------------------------------------------------------------
        | Backup / database file di public_html
        |--------------------------------------------------------------------------
        */
        if (in_array($ext, $backupExt, true)) {
            add_issue(
                $issues,
                'TINGGI',
                $relative,
                0,
                'File backup/database berada di public_html',
                'File backup seperti .sql, .zip, .bak bisa dicuri dari browser jika tidak terblokir.',
                'Pindahkan file ini keluar public_html atau hapus jika sudah tidak dipakai.',
                ''
            );
        }

        /*
        |--------------------------------------------------------------------------
        | PHP di folder asset/upload/images/css/js
        |--------------------------------------------------------------------------
        */
        if (in_array($ext, $phpExt, true)) {
            $totalPhpFiles++;

            if (preg_match('#(^|/)(asset|assets|upload|uploads|images|img|css|js)(/|$)#i', $relativeUnix)) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    0,
                    'File PHP berada di folder asset/upload/images/css/js',
                    'Ini sangat sering menjadi webshell hasil upload hacker.',
                    'Cek isi file. Jika bukan file sistem yang kamu kenal, karantina/hapus setelah backup.',
                    ''
                );
            }
        }

        /*
        |--------------------------------------------------------------------------
        | Permission world writable
        |--------------------------------------------------------------------------
        */
        if (($file->getPerms() & 0x0002) !== 0) {
            add_issue(
                $issues,
                'TINGGI',
                $relative,
                0,
                'Permission file world-writable',
                'File bisa ditulis oleh user lain di server. Ini berbahaya di shared hosting.',
                'Ubah permission file ke 644 dan folder ke 755.',
                ''
            );
        }

        /*
        |--------------------------------------------------------------------------
        | File baru berubah
        |--------------------------------------------------------------------------
        */
        if ($mtime >= strtotime('-7 days') && in_array($ext, $phpExt, true)) {
            add_issue(
                $issues,
                'RENDAH',
                $relative,
                0,
                'File PHP berubah dalam 7 hari terakhir',
                'Bukan pasti hack, tapi perlu dicek jika kamu tidak merasa mengedit file ini.',
                'Bandingkan dengan backup bersih atau cek isi file.',
                date('Y-m-d H:i:s', $mtime)
            );
        }

        /*
        |--------------------------------------------------------------------------
        | Hanya baca file text/php kecil
        |--------------------------------------------------------------------------
        */
        $readableExt = array_merge($phpExt, ['js', 'html', 'htm', 'css', 'htaccess', 'ini', 'env', 'txt']);

        if (!in_array($ext, $readableExt, true)) {
            continue;
        }

        if ($size > $maxReadSize) {
            add_issue(
                $issues,
                'SEDANG',
                $relative,
                0,
                'File terlalu besar untuk discan penuh',
                'File lebih dari 2MB. Backdoor kadang disisipkan di file besar.',
                'Buka manual file ini dan cek bagian awal/akhir file.',
                number_format($size) . ' bytes'
            );
            continue;
        }

        if (!is_readable($path)) {
            add_issue(
                $issues,
                'SEDANG',
                $relative,
                0,
                'File tidak bisa dibaca scanner',
                'Scanner tidak punya akses baca ke file ini.',
                'Cek permission file lewat File Manager.',
                ''
            );
            continue;
        }

        $content = (string)@file_get_contents($path);
        $lines = preg_split('/\R/', $content);

        /*
        |--------------------------------------------------------------------------
        | File level checks
        |--------------------------------------------------------------------------
        */
        if (preg_match('/(FilesMan|WSO|C99|R57|b374k|IndoXploit|AnonymousFox|Alfa Shell|Mini Shell)/i', $content)) {
            add_issue(
                $issues,
                'TINGGI',
                $relative,
                0,
                'Signature webshell ditemukan',
                'Isi file mengandung nama/signature webshell populer.',
                'Segera karantina file ini. Jangan akses dari browser.',
                ''
            );
        }

        if (preg_match('/[A-Za-z0-9+\/]{400,}={0,2}/', $content)) {
            add_issue(
                $issues,
                'SEDANG',
                $relative,
                0,
                'String encoded sangat panjang',
                'Backdoor sering menyembunyikan payload dalam base64 panjang.',
                'Cek apakah string ini memang asset/data normal. Jika digabung eval/gzinflate, sangat berbahaya.',
                ''
            );
        }

        if (preg_match('/move_uploaded_file\s*\(/i', $content)) {
            if (
                !preg_match('/finfo_file|mime_content_type|getimagesize/i', $content)
            ) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    0,
                    'Upload file tanpa validasi MIME kuat',
                    'File upload harus dicek MIME asli, bukan hanya ekstensi.',
                    'Tambahkan validasi finfo_file/getimagesize untuk jpg/png/webp.',
                    ''
                );
            }

            if (
                !preg_match('/in_array\s*\(.+?(jpg|jpeg|png|webp)/is', $content)
            ) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    0,
                    'Upload file tanpa whitelist ekstensi',
                    'Upload tanpa whitelist bisa membuat hacker upload .php/.phtml.',
                    'Batasi ekstensi hanya jpg, jpeg, png, webp dan buat nama file random.',
                    ''
                );
            }

            if (preg_match('/move_uploaded_file\s*\(.+?\$_FILES.+?\[\s*[\'"]name[\'"]\s*\]/is', $content)) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    0,
                    'Upload memakai nama file asli user',
                    'Nama file asli bisa berisi karakter berbahaya atau ekstensi palsu.',
                    'Gunakan nama random: bin2hex(random_bytes(16)) . ".jpg"',
                    ''
                );
            }
        }

        if (preg_match('/\$_POST|\$_REQUEST/i', $content) && !preg_match('/csrf|token/i', $content)) {
            $risk = (stripos($relativeUnix, 'uvgaming/') !== false) ? 'TINGGI' : 'SEDANG';

            add_issue(
                $issues,
                $risk,
                $relative,
                0,
                'Handler POST tanpa CSRF token',
                'Form POST tanpa CSRF bisa dipaksa jalan oleh halaman lain jika admin/user sedang login.',
                'Tambahkan session csrf_token dan validasi sebelum proses POST.',
                ''
            );
        }

        if (stripos($relativeUnix, 'uvgaming/') !== false && in_array($ext, $phpExt, true)) {
            $isLoginFile = preg_match('/(login|logout|auth)/i', $base);

            if (!$isLoginFile && !preg_match('/kode_admin|admin|level|role|hak_akses/i', $content)) {
                add_issue(
                    $issues,
                    'SEDANG',
                    $relative,
                    0,
                    'File admin tidak terlihat punya pengecekan session admin',
                    'File di folder uvgaming harus memastikan admin sudah login.',
                    'Tambahkan validasi: if (!isset($_SESSION["kode_admin"])) exit/redirect login.',
                    ''
                );
            }
        }

        /*
        |--------------------------------------------------------------------------
        | Line by line checks
        |--------------------------------------------------------------------------
        */
        $superVars = [];

        foreach ($lines as $index => $line) {
            $lineNo = $index + 1;
            $trim = trim($line);

            if ($trim === '') {
                continue;
            }

            /*
            | Simpan variable dari input user
            */
            if (preg_match('/\$(\w+)\s*=\s*\$_(GET|POST|REQUEST|COOKIE)\s*\[/i', $trim, $m)) {
                $superVars[$m[1]] = [
                    'source' => $m[2],
                    'line' => $lineNo,
                ];
            }

            /*
            | Dangerous execution
            */
            $dangerRules = [
                '/\beval\s*\(/i' => ['TINGGI', 'eval() ditemukan', 'eval() sering dipakai backdoor untuk menjalankan kode tersembunyi.', 'Hapus eval(). Jika untuk parsing data, ganti dengan json_decode atau logic normal.'],
                '/\bassert\s*\(/i' => ['TINGGI', 'assert() ditemukan', 'assert() bisa dipakai eksekusi kode pada beberapa versi PHP.', 'Hapus assert() dari production.'],
                '/\bshell_exec\s*\(/i' => ['TINGGI', 'shell_exec() ditemukan', 'Bisa menjalankan perintah server.', 'Hapus jika tidak wajib. Jika wajib, whitelist command dan jangan pakai input user.'],
                '/\bexec\s*\(/i' => ['TINGGI', 'exec() ditemukan', 'Bisa menjalankan perintah server.', 'Hapus jika tidak wajib.'],
                '/\bsystem\s*\(/i' => ['TINGGI', 'system() ditemukan', 'Bisa menjalankan perintah server.', 'Hapus jika tidak wajib.'],
                '/\bpassthru\s*\(/i' => ['TINGGI', 'passthru() ditemukan', 'Bisa menjalankan perintah server.', 'Hapus jika tidak wajib.'],
                '/\bproc_open\s*\(/i' => ['TINGGI', 'proc_open() ditemukan', 'Bisa menjalankan proses server.', 'Hapus jika tidak wajib.'],
                '/\bpopen\s*\(/i' => ['TINGGI', 'popen() ditemukan', 'Bisa menjalankan proses server.', 'Hapus jika tidak wajib.'],
                '/preg_replace\s*\(.+?\/e/is' => ['TINGGI', 'preg_replace /e ditemukan', 'Modifier /e berbahaya dan deprecated.', 'Ganti dengan preg_replace_callback().'],
                '/create_function\s*\(/i' => ['TINGGI', 'create_function() ditemukan', 'Fungsi ini deprecated dan bisa berbahaya.', 'Ganti dengan anonymous function biasa.'],
            ];

            foreach ($dangerRules as $regex => $rule) {
                if (preg_match($regex, $trim)) {
                    add_issue($issues, $rule[0], $relative, $lineNo, $rule[1], $rule[2], $rule[3], $trim);
                }
            }

            /*
            | Obfuscation
            */
            if (preg_match('/base64_decode\s*\(/i', $trim)) {
                $level = preg_match('/eval|gzinflate|gzuncompress|str_rot13/i', $content) ? 'TINGGI' : 'SEDANG';

                add_issue(
                    $issues,
                    $level,
                    $relative,
                    $lineNo,
                    'base64_decode() ditemukan',
                    'Sering dipakai untuk menyembunyikan kode. Tidak selalu hack, tapi wajib dicek.',
                    'Pastikan bukan payload backdoor. Jika dipakai untuk data biasa, beri komentar jelas.',
                    $trim
                );
            }

            if (preg_match('/gzinflate\s*\(|gzuncompress\s*\(|str_rot13\s*\(/i', $trim)) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    $lineNo,
                    'Obfuscation function ditemukan',
                    'Fungsi ini sering dipakai backdoor untuk menyembunyikan kode.',
                    'Cek isi file. Jika digabung eval/base64, kemungkinan besar backdoor.',
                    $trim
                );
            }

            /*
            | Include/require dari input user
            */
            if (preg_match('/(include|require|include_once|require_once)\s*\(?\s*\$_(GET|POST|REQUEST|COOKIE)/i', $trim)) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    $lineNo,
                    'Include/require dari input user',
                    'Ini bisa menyebabkan Local File Inclusion / Remote Code Execution.',
                    'Gunakan whitelist nama halaman, jangan langsung include dari $_GET/$_POST.',
                    $trim
                );
            }

            /*
            | SQL injection direct
            */
            if (
                preg_match('/(mysqli_query|\->query|PDO::query)\s*\(/i', $trim) &&
                preg_match('/\$_(GET|POST|REQUEST|COOKIE)/i', $trim)
            ) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    $lineNo,
                    'SQL query langsung memakai input user',
                    'Ini rawan SQL Injection.',
                    'Ganti ke prepared statement mysqli_prepare / bind_param.',
                    $trim
                );
            }

            if (
                preg_match('/(mysqli_query|\->query|PDO::query)\s*\(/i', $trim) &&
                preg_match('/[\'"].*\$[a-zA-Z_]\w*.*[\'"]/i', $trim)
            ) {
                add_issue(
                    $issues,
                    'SEDANG',
                    $relative,
                    $lineNo,
                    'SQL query memakai variable langsung',
                    'Jika variable berasal dari input user, ini rawan SQL Injection.',
                    'Pastikan variable sudah divalidasi ketat atau ubah ke prepared statement.',
                    $trim
                );
            }

            foreach ($superVars as $varName => $meta) {
                if (
                    preg_match('/(mysqli_query|\->query|PDO::query)\s*\(/i', $trim) &&
                    preg_match('/\$' . preg_quote($varName, '/') . '\b/', $trim)
                ) {
                    add_issue(
                        $issues,
                        'TINGGI',
                        $relative,
                        $lineNo,
                        'SQL Injection dari variable input user',
                        'Variable $' . $varName . ' berasal dari $_' . $meta['source'] . ' baris ' . $meta['line'] . ' lalu dipakai di query.',
                        'Ubah query ke prepared statement dan validasi tipe data.',
                        $trim
                    );
                }
            }

            /*
            | XSS direct echo
            */
            if (preg_match('/echo\s+\$_(GET|POST|REQUEST|COOKIE)/i', $trim)) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    $lineNo,
                    'Output langsung dari input user',
                    'Ini rawan XSS.',
                    'Bungkus output dengan htmlspecialchars($value, ENT_QUOTES, "UTF-8").',
                    $trim
                );
            }

            if (preg_match('/<\?=\s*\$_(GET|POST|REQUEST|COOKIE)/i', $trim)) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    $lineNo,
                    'Short echo langsung dari input user',
                    'Ini rawan XSS.',
                    'Ubah menjadi <?= htmlspecialchars($value, ENT_QUOTES, "UTF-8"); ?>',
                    $trim
                );
            }

            /*
            | Redirect dari input user
            */
            if (preg_match('/header\s*\(\s*[\'"]Location:\s*[\'"]\s*\.\s*\$_(GET|POST|REQUEST|COOKIE)/i', $trim)) {
                add_issue(
                    $issues,
                    'SEDANG',
                    $relative,
                    $lineNo,
                    'Redirect memakai input user',
                    'Bisa menjadi open redirect.',
                    'Batasi redirect hanya ke path internal yang di-whitelist.',
                    $trim
                );
            }

            /*
            | File write/delete dari input user
            */
            if (
                preg_match('/(unlink|file_put_contents|fopen|rename|copy)\s*\(/i', $trim) &&
                preg_match('/\$_(GET|POST|REQUEST|COOKIE)/i', $trim)
            ) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    $lineNo,
                    'Operasi file memakai input user',
                    'Bisa dipakai hapus/tulis file sembarang.',
                    'Gunakan whitelist folder, basename(), realpath(), dan cek path tetap di folder aman.',
                    $trim
                );
            }

            /*
            | unserialize user input
            */
            if (preg_match('/unserialize\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)/i', $trim)) {
                add_issue(
                    $issues,
                    'TINGGI',
                    $relative,
                    $lineNo,
                    'unserialize() dari input user',
                    'Rawan PHP Object Injection.',
                    'Ganti dengan json_decode() atau unserialize dengan allowed_classes=false.',
                    $trim
                );
            }

            /*
            | display errors on
            */
            if (preg_match('/ini_set\s*\(\s*[\'"]display_errors[\'"]\s*,\s*[\'"]?1[\'"]?/i', $trim)) {
                add_issue(
                    $issues,
                    'SEDANG',
                    $relative,
                    $lineNo,
                    'display_errors aktif',
                    'Error detail bisa membocorkan path, query, dan credential.',
                    'Matikan di production: ini_set("display_errors","0"). Log error ke file internal.',
                    $trim
                );
            }

            /*
            | Hardcoded credentials
            */
            if (preg_match('/(password|passwd|secret|api_key|apikey|token)\s*=\s*[\'"][^\'"]{6,}[\'"]/i', $trim)) {
                add_issue(
                    $issues,
                    'RENDAH',
                    $relative,
                    $lineNo,
                    'Kemungkinan credential hardcoded',
                    'Credential di file public rawan bocor jika file terdownload.',
                    'Simpan di config yang diblokir akses publik dan rotasi jika pernah kena hack.',
                    $trim
                );
            }
        }
    }
} catch (Throwable $e) {
    exit('Scanner error: ' . h($e->getMessage()));
}

usort($issues, function ($a, $b) {
    if ($a['score'] === $b['score']) {
        return strcmp($a['file'], $b['file']);
    }

    return $b['score'] <=> $a['score'];
});

$countHigh = 0;
$countMed = 0;
$countLow = 0;

foreach ($issues as $i) {
    if ($i['level'] === 'TINGGI') $countHigh++;
    elseif ($i['level'] === 'SEDANG') $countMed++;
    else $countLow++;
}

$filter = $_GET['level'] ?? 'all';

$shownIssues = array_filter($issues, function ($i) use ($filter) {
    if ($filter === 'all') return true;
    return strtolower($i['level']) === strtolower($filter);
});

?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Security Audit File</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
    background: #080808;
    color: #eee;
    font-family: Arial, sans-serif;
    padding: 20px;
    margin: 0;
}
.wrap {
    max-width: 1400px;
    margin: 0 auto;
}
h1 {
    color: #ffcc00;
    margin: 0 0 10px;
}
.note {
    background: #201500;
    border: 1px solid #8a6500;
    color: #ffe3a0;
    padding: 12px;
    border-radius: 10px;
    margin: 15px 0;
}
.cards {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    margin: 15px 0;
}
.card {
    background: #121212;
    border: 1px solid #333;
    border-radius: 12px;
    padding: 14px;
    min-width: 150px;
}
.card b {
    display: block;
    font-size: 24px;
    color: #fff;
}
.card span {
    color: #aaa;
    font-size: 12px;
}
.filters {
    margin: 15px 0;
}
.btn {
    display: inline-block;
    padding: 10px 14px;
    margin: 0 5px 8px 0;
    border-radius: 8px;
    text-decoration: none;
    background: #222;
    color: #fff;
    border: 1px solid #444;
    font-weight: bold;
    font-size: 13px;
}
.btn.active {
    background: #ffcc00;
    color: #000;
    border-color: #ffcc00;
}
table {
    width: 100%;
    border-collapse: collapse;
    background: #111;
    border: 1px solid #333;
}
th, td {
    border-bottom: 1px solid #333;
    padding: 10px;
    vertical-align: top;
    font-size: 13px;
}
th {
    background: #1b1b1b;
    color: #ffcc00;
    text-align: left;
}
.level-TINGGI {
    color: #ff4d4d;
    font-weight: bold;
}
.level-SEDANG {
    color: #ffaa00;
    font-weight: bold;
}
.level-RENDAH {
    color: #42c8ff;
    font-weight: bold;
}
.path {
    font-family: Consolas, monospace;
    color: #fff;
    word-break: break-all;
}
.snippet {
    background: #050505;
    border: 1px solid #333;
    padding: 8px;
    border-radius: 8px;
    color: #ddd;
    font-family: Consolas, monospace;
    font-size: 12px;
    white-space: pre-wrap;
    word-break: break-word;
    margin-top: 6px;
}
.fix {
    color: #b8ffb8;
}
.small {
    color: #aaa;
    font-size: 12px;
}
</style>
</head>
<body>
<div class="wrap">
    <h1>Security Audit File</h1>
    <div class="small">Root: <?= h($root); ?></div>

    <div class="note">
        Ini scanner audit read-only. Hasil “TINGGI” harus diprioritaskan.
        Jangan hapus file sebelum dicek dan backup.
    </div>

    <div class="cards">
        <div class="card">
            <b><?= number_format($totalFiles); ?></b>
            <span>Total file dicek</span>
        </div>
        <div class="card">
            <b><?= number_format($totalPhpFiles); ?></b>
            <span>File PHP</span>
        </div>
        <div class="card">
            <b><?= number_format($countHigh); ?></b>
            <span>Risiko Tinggi</span>
        </div>
        <div class="card">
            <b><?= number_format($countMed); ?></b>
            <span>Risiko Sedang</span>
        </div>
        <div class="card">
            <b><?= number_format($countLow); ?></b>
            <span>Risiko Rendah</span>
        </div>
    </div>

    <div class="filters">
        <a class="btn <?= $filter === 'all' ? 'active' : ''; ?>" href="?level=all">Semua</a>
        <a class="btn <?= $filter === 'TINGGI' ? 'active' : ''; ?>" href="?level=TINGGI">Tinggi</a>
        <a class="btn <?= $filter === 'SEDANG' ? 'active' : ''; ?>" href="?level=SEDANG">Sedang</a>
        <a class="btn <?= $filter === 'RENDAH' ? 'active' : ''; ?>" href="?level=RENDAH">Rendah</a>
        <a class="btn" href="?refresh=<?= time(); ?>">Scan Ulang</a>
    </div>

    <table>
        <thead>
            <tr>
                <th style="width:80px;">Risk</th>
                <th style="width:70px;">Line</th>
                <th style="width:260px;">File</th>
                <th>Masalah</th>
                <th>Solusi</th>
            </tr>
        </thead>
        <tbody>
        <?php if (empty($shownIssues)): ?>
            <tr>
                <td colspan="5">Tidak ada hasil untuk filter ini.</td>
            </tr>
        <?php else: ?>
            <?php foreach ($shownIssues as $i): ?>
                <tr>
                    <td class="level-<?= h($i['level']); ?>"><?= h($i['level']); ?></td>
                    <td><?= $i['line'] > 0 ? (int)$i['line'] : '-'; ?></td>
                    <td class="path"><?= h($i['file']); ?></td>
                    <td>
                        <b><?= h($i['title']); ?></b><br>
                        <span class="small"><?= h($i['detail']); ?></span>
                        <?php if ($i['snippet'] !== ''): ?>
                            <div class="snippet"><?= h($i['snippet']); ?></div>
                        <?php endif; ?>
                    </td>
                    <td class="fix"><?= h($i['fix']); ?></td>
                </tr>
            <?php endforeach; ?>
        <?php endif; ?>
        </tbody>
    </table>

    <div class="note">
        Setelah selesai dipakai, lebih aman rename file ini menjadi nama rahasia
        atau hapus dari server. Jangan biarkan tool audit terbuka untuk orang lain.
    </div>
</div>
</body>
</html>