Sunucuyu Hackerlardan Önce Tarama

Kutay Utku
77 dk okuma

Bir web sitesi işletmenin en can sıkıcı yanı, “acaba sunucumda gözden kaçırdığım bir açık var mı?” sorusuyla sürekli yaşamaktır. Apache’nin sürümü güncel mi? PHP’de display_errors hâlâ açık mı kaldı? phpMyAdmin varsayılan adresinde mi duruyor? .git klasörü yanlışlıkla dışarıya mı açık? Bu soruların çoğu, elle kontrol edilmesi unutulan ama saldırganların ilk bakacağı yerlerdir.

Bu ihtiyaçtan yola çıkarak tek dosyalık, bağımlılıksız bir PHP betiği olan acik.php’yi hazırladım. Sunucunuza atıp tarayıcıdan açtığınız anda, sunucunuzun güvenlik durumunu tek bir panelde özetliyor.

Ne İşe Yarıyor?

acik.php gerçek bir saldırı ya da exploit denemesi yapmaz. Yaptığı şey tamamen pasif: sunucunuzdaki yazılım sürümlerini, PHP yapılandırmasını, dosya izinlerini ve HTTP başlıklarını okuyup bilinen risk kalıplarıyla karşılaştırmak. Kontrol ettiği başlıca alanlar:

  • Sürüm tespiti: Apache, Nginx, PHP, MySQL/MariaDB, OpenSSL, cURL, Redis, PostgreSQL, ImageMagick gibi bileşenlerin sürümünü tespit edip eskiyse uyarıyor.
  • Yapılandırma hataları: expose_php, display_errors, allow_url_include, session.cookie_secure gibi php.ini ayarlarının üretim ortamına uygun olup olmadığını kontrol ediyor.
  • Dosya ve dizin ifşaları: .git, .env, vendor/, node_modules/, .DS_Store gibi normalde web kökünde görünmemesi gereken dosyaları arıyor.
  • Şüpheli içerik taraması: eval(base64_decode(…)) gibi bilinen web shell/obfuscation imzalarını belge kökündeki PHP dosyalarında tarıyor.
  • HTTP güvenlik başlıkları: HSTS, CSP, X-Frame-Options gibi eksik başlıkları listeliyor.
  • Ağ servisleri: MySQL, Redis, MongoDB gibi portların 0.0.0.0 üzerinde herkese açık dinleyip dinlemediğini kontrol ediyor.
  • CMS tespiti: WordPress/Joomla/Drupal kurulu mu, sürümü ifşa oluyor mu, xmlrpc.php veya REST API üzerinden kullanıcı adı sızdırılıyor mu bakıyor.
  • Sistem bilgisi: PHP hangi kullanıcıyla çalışıyor, root mu, güvenlik duvarı aktif mi, disk/RAM/CPU durumu ne gibi genel bir özet sunuyor.

Her bulguyu İYİ, DİKKAT veya RİSKLİ olarak etiketleyip toplam bir güvenlik skoru hesaplıyor; en kritik üç maddeyi de sayfanın üstünde “önce bunlara bakın” kutusunda ayrıca gösteriyor.

Neden Böyle Bir Araca İhtiyaç Var?

Çoğu güvenlik ihlali, karmaşık sıfır gün açıklarından değil, aylarca fark edilmeyen basit yapılandırma hatalarından kaynaklanır: eski bir PHP sürümü, dışarıya açık bir .git klasörü, unutulmuş bir phpMyAdmin kurulumu… acik.php bu tür “düşük asılı meyveleri” birkaç saniyede gözler önüne seriyor ve sizi ilgili CVE veritabanlarına (cve.org, nvd.nist.gov) yönlendiriyor ki sürüm bazlı riskleri kendiniz teyit edebilesiniz.

Kurulum

  1. acik.php dosyasını sunucunuzun web kökü altına (ya da erişimi kısıtlayabileceğiniz bir alt dizine) yükleyin.
  2. Aynı dizine yazılabilir bir acik_giris.log dosyası bırakın; script giriş denemelerini buraya kaydediyor.
  3. Dosyanın başındaki $PANEL_PASSWORD değişkenini MUTLAKA değiştirin. Varsayılan parola sadece örnek amaçlıdır ve tahmin edilmesi çok kolaydır.
  4. Tarayıcıdan sunucunuz/acik.php adresine gidip yeni parolanızla giriş yapın.
  5. Raporu inceleyip RİSKLİ maddeleri kapatın, sonra dosyayı sunucudan silin.

Dosyalar Tam Olarak Nereye Kaydedilmeli?

acik.php içindeki $LOG_DOSYASI satırı, log dosyasını her zaman acik.php ile AYNI DİZİNDE arar (DIR . ‘/acik_giris.log’). Yani iki dosyayı da aynı klasöre koymanız yeterli, farklı yerlere dağıtmayın.

Hangi dizin olduğu, sunucunuzun türüne göre değişir:

  • Paylaşımlı hosting (cPanel / Plesk / DirectAdmin): Genelde public_html/ (bazı sağlayıcılarda htdocs/ veya www/) klasörü web kökünüzdür. Dosya Yöneticisi (File Manager) veya FTP ile:
  • acik.php → public_html/acik.php
  • acik_giris.log → public_html/acik_giris.log (boş bir dosya olarak oluşturun, script kendisi doldurur)
    Not: cPanel’de “Sadece bana görünsün” istiyorsanız public_html/gizli-bir-klasor-adi/acik.php gibi tahmin edilmesi zor bir alt klasöre de koyabilirsiniz.
  • Linux VPS / kendi sunucunuz (Apache): Apache’nin DocumentRoot’unun gösterdiği klasör hedefiniz. Genelde:
  • /var/www/html/acik.php
  • /var/www/html/acik_giris.log
    veya siteniz için ayrı bir vhost tanımlıysa /var/www/siteadi.com/public_html/acik.php gibi bir yol olur. Emin değilseniz sunucuda şu komutla kontrol edin: apache2ctl -S ya da ilgili .conf dosyanızdaki DocumentRoot satırına bakın.
  • Linux VPS (Nginx + PHP-FPM): Nginx yapılandırmanızdaki root satırının gösterdiği klasör (örn. /var/www/siteadi.com/public gibi) hedefiniz; genelde nginx.conf veya /etc/nginx/sites-available/siteadi içinde “root …” satırında yazar.
  • Windows Server / IIS: Genelde C:\inetpub\wwwroot\ klasörüdür.
  • C:\inetpub\wwwroot\acik.php
  • C:\inetpub\wwwroot\acik_giris.log

acik_giris.log’u Nasıl Oluşturmalıyım?

Script, dosya yoksa ilk giriş denemesinde onu kendisi otomatik olarak oluşturur (fopen($LOG_DOSYASI, ‘c+’) satırı bunu yapıyor) — yani elle oluşturmasanız da çalışır. Ama bazı paylaşımlı hosting ortamlarında PHP’nin o dizine yazma izni olmayabilir; bu durumda “Kayıt yok” hatası alırsınız. Sorun yaşarsanız:

  1. FTP/Dosya Yöneticisi üzerinden acik.php ile aynı klasörde boş bir acik_giris.log dosyası oluşturun (içeriği boş bırakabilirsiniz).
  2. Dosya izinlerini 644 (bazı paylaşımlı hostinglerde 664 veya 666) yapın; klasör izninin de PHP’nin yazabileceği şekilde (genelde 755) olduğundan emin olun.
  3. Hâlâ yazamıyorsa hosting sağlayıcınızın PHP’yi hangi kullanıcıyla çalıştırdığını (www-data, PHP-FPM pool kullanıcısı vb.) kontrol edip o kullanıcıya yazma izni verin.

Özetle: iki dosyayı da web kökünüzdeki AYNI klasöre atın, acik_giris.log’u boş bırakabilirsiniz, gerekirse script kendisi dolduracaktır.

Örnek: Değiştirmeniz Gereken Ayarlar Bölümü

acik.php’nin en başında, dosyayı sunucuya yüklemeden önce düzenlemeniz gereken ayarlar şu şekilde görünüyor:

/* ================= AYARLAR ================= */
// DEPLOY ETMEDEN ONCE PAROLAYI DEGISTIRIN.
$PANEL_PASSWORD = '1234566';
$MAX_DENEME = 5;              // kilitlenmeden önce izin verilen yanlış deneme sayisi
$KILIT_SURESI = 300;          // saniye (5 dakika)
$OTURUM_ZAMAN_ASIMI = 600;    // saniye (10 dakika hareketsizlikte otomatik çıkış)
$LOG_DOSYASI = __DIR__ . '/acik_giris.log';
$IZINLI_IPLER = [];           // bos = kısıtlama yok. Örnek: ['85.34.12.9', '::1']
/* ============================================= */

Burada mutlaka değiştirmeniz gereken satır $PANEL_PASSWORD. Sadece kendi IP’nizden erişmek isterseniz $IZINLI_IPLER dizisine kendi IP adresinizi de yazabilirsiniz, örneğin: $IZINLI_IPLER = [‘85.34.12.9’];

Örnek: acik_giris.log İçeriği Nasıl Görünür?

Script her giriş denemesini otomatik olarak bu dosyaya yazar; her satır sekme (tab) ile ayrılmış 6 alandan oluşur: tarih-saat, olay, sonuç, IP, sebep, tarayıcı. Elle bir şey yazmanıza gerek yok, sadece örnek olsun diye:

2026-08-08 14:02:11    GIRIS   BASARILI    203.0.113.10    normal  Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
2026-08-08 14:01:47    GIRIS   BASARISIZ   203.0.113.10    yanlış parola   Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
2026-08-08 14:05:00    CIKIS   BASARILI    203.0.113.10    çıkış yapıldı   Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...

Bu satırları panel içindeki “Son Giriş Denemeleri” tablosu otomatik olarak okuyup listeliyor; dosyayı elle düzenlemenize gerek yok.

Güvenlik Notları (Atlamayın)

Bu panelin kendisi de, doğası gereği hassas bilgi gösterdiği için dikkatli kullanılmalı:

  • Varsayılan parolayı değiştirmeden asla canlıya almayın. Panel, sunucu hakkında saldırganlar için de değerli olan bilgiler gösteriyor; zayıf parolayla bırakmak amacın tam tersine hizmet eder.
  • Mümkünse $IZINLI_IPLER listesini kendi IP’nizle sınırlayın; böylece panel sadece sizin erişebileceğiniz bir kaynak hâline gelir.
  • İşiniz bittiğinde dosyayı sunucudan silin. İçindeki brute-force kilidi (5 yanlış denemede 5 dakika kilitleme) ve CSRF koruması ek bir güvenlik katmanı sağlasa da, kalıcı olarak sunucuda durmasının bir gerekçesi yok.
  • acik_giris.log dosyası her giriş denemesini IP ve tarayıcı bilgisiyle kaydediyor; bu da olası kaba kuvvet denemelerini geriye dönük incelemenize imkân tanıyor.

Sonuç

acik.php, büyük ve karmaşık güvenlik tarama araçlarının yerini tutmaz; ama beş dakikanızı alacak, kurulumu tek dosyadan ibaret, sunucunuzun “temel hijyenini” hızlıca kontrol etmenizi sağlayan pratik bir araç. Düzenli olarak (örneğin her ay) çalıştırıp raporu gözden geçirmek, çoğu fırsatçı saldırının işini daha en baştan zorlaştırır.

Unutmayın: gerçek güvenlik, tek seferlik bir tarama değil, sürekli bir alışkanlıktır.

Ek: acik.php Tam Kaynak Kodu

Merak edenler ve doğrudan inceleyip kendi sunucusuna uyarlamak isteyenler için aracın tam kaynak kodunu da aşağıya bırakıyorum. Kod bağımlılıksızdır, tek dosyadır ve doğrudan çalıştırılabilir; kopyalayıp acik.php adıyla kaydetmeniz yeterlidir (yukarıdaki “Dosyalar Tam Olarak Nereye Kaydedilmeli?” bölümündeki yol bilgisine göre).

<?php
/*
 * acik.php - Apache2 / PHP / MySQL Güvenlik Denetim Paneli (v2)
 *
 * Sunucuda çalışan Apache, PHP ve MySQL surumlerini tespit eder,
 * bilinen (yayinlanmis) güvenlik açığı aralıklarına karşı KARSILASTIRIR,
 * yaygin yanlış konfigürasyonları, hassas/açık kalmis dosyaları ve
 * eksik HTTP güvenlik basliklarini kontrol eder. GERCEK BIR SALDIRI /
 * EXPLOIT DENEMESI YAPMAZ - sadece bilgi toplar ve sizi CVE veritabanina
 * yönlendirir.
 *
 * GÜVENLİK: Panel basit bir parola ile korunur (aşağıda $PANEL_PASSWORD).
 * DEPLOY ETMEDEN ONCE PAROLAYI DEGISTIRIN. Kullanım bitince dosyayı
 * sunucudan silmeniz önerilir.
 */

session_start();
error_reporting(E_ALL);
ini_set('display_errors', '1');

/* ================= AYARLAR ================= */
// DEPLOY ETMEDEN ONCE PAROLAYI DEGISTIRIN.
$PANEL_PASSWORD = '1234566';
$MAX_DENEME = 5;              // kilitlenmeden önce izin verilen yanlış deneme sayisi
$KILIT_SURESI = 300;          // saniye (5 dakika)
$OTURUM_ZAMAN_ASIMI = 600;    // saniye (10 dakika hareketsizlikte otomatik çıkış)
$LOG_DOSYASI = __DIR__ . '/acik_giris.log';
$IZINLI_IPLER = [];           // bos = kısıtlama yok. Örnek: ['85.34.12.9', '::1']
/* ============================================= */

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

function girisLogla($LOG_DOSYASI, $olay, $sonuc, $ip, $sebep = '-', $userAgent = '-') {
    $userAgent = $userAgent ?: '-';
    $userAgent = str_replace(["\t", "\n", "\r"], ' ', $userAgent);
    $satir = implode("\t", [
        date('Y-m-d H:i:s'),
        $olay,
        $sonuc,
        $ip,
        $sebep ?: '-',
        mb_substr($userAgent, 0, 150),
    ]);

    // Yeni kayıt dosyanın EN BASINA eklenir (en yeni en ustte), toplam kayıt MAX_KAYIT ile sinirlanir.
    $MAX_KAYIT = 5000;
    $fp = @fopen($LOG_DOSYASI, 'c+');
    if ($fp === false) return;
    if (flock($fp, LOCK_EX)) {
        $eskiIcerik = stream_get_contents($fp) ?: '';
        $eskiSatirlar = $eskiIcerik !== '' ? explode("\n", rtrim($eskiIcerik, "\n")) : [];
        array_unshift($eskiSatirlar, $satir);
        if (count($eskiSatirlar) > $MAX_KAYIT) {
            $eskiSatirlar = array_slice($eskiSatirlar, 0, $MAX_KAYIT);
        }
        ftruncate($fp, 0);
        rewind($fp);
        fwrite($fp, implode("\n", $eskiSatirlar) . "\n");
        flock($fp, LOCK_UN);
    }
    fclose($fp);
}

$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'bilinmiyor';
$userAgentStr = $_SERVER['HTTP_USER_AGENT'] ?? '-';

// --- IP kısıtlaması ---
if ($IZINLI_IPLER && !in_array($clientIp, $IZINLI_IPLER, true)) {
    girisLogla($LOG_DOSYASI, 'IP_ENGEL', 'BASARISIZ', $clientIp, 'izin verilmeyen IP', $userAgentStr);
    http_response_code(403);
    echo '<!DOCTYPE html><html lang="tr"><head><meta charset="UTF-8"><title>Erişim Engellendi</title>'
       . '<style>body{background:#0b0f1e;color:#d8dee9;font-family:Consolas,monospace;'
       . 'display:flex;align-items:center;justify-content:center;height:100vh;margin:0}</style></head>'
       . '<body><div>403 - Bu IP adresine izin verilmiyor.</div></body></html>';
    exit;
}

// --- Oturum zaman asimi ---
if (!empty($_SESSION['acik_auth']) && !empty($_SESSION['acik_son_islem'])) {
    if (time() - $_SESSION['acik_son_islem'] > $OTURUM_ZAMAN_ASIMI) {
        unset($_SESSION['acik_auth']);
        $timeoutMsg = true;
        girisLogla($LOG_DOSYASI, 'OTURUM', 'BASARISIZ', $clientIp, 'hareketsizlik nedeniyle oturum sonlandı', $userAgentStr);
    }
}
$_SESSION['acik_son_islem'] = time();

// --- CSRF token ---
if (empty($_SESSION['acik_csrf'])) {
    $_SESSION['acik_csrf'] = bin2hex(random_bytes(16));
}

// --- Brute-force kilidi ---
$kilitliMi = false;
$kalanSure = 0;
if (!empty($_SESSION['acik_kilit_zamani'])) {
    $geçen = time() - $_SESSION['acik_kilit_zamani'];
    if ($geçen < $KILIT_SURESI) {
        $kilitliMi = true;
        $kalanSure = $KILIT_SURESI - $geçen;
    } else {
        unset($_SESSION['acik_kilit_zamani'], $_SESSION['acik_deneme']);
    }
}

// --- Basit oturum koruması ---
if (isset($_POST['panel_password']) && $kilitliMi) {
    girisLogla($LOG_DOSYASI, 'GIRIS', 'BASARISIZ', $clientIp, 'kilitliyken deneme', $userAgentStr);
}
if (isset($_POST['panel_password']) && !$kilitliMi) {
    $csrfOk = isset($_POST['csrf']) && hash_equals($_SESSION['acik_csrf'], $_POST['csrf']);
    if (!$csrfOk) {
        $loginError = 'Oturum doğruluması basarisiz, sayfayi yenileyip tekrar deneyin.';
        girisLogla($LOG_DOSYASI, 'GIRIS', 'BASARISIZ', $clientIp, 'csrf doğrulama hatası', $userAgentStr);
    } elseif (hash_equals($PANEL_PASSWORD, $_POST['panel_password'])) {
        $_SESSION['acik_auth'] = true;
        unset($_SESSION['acik_deneme'], $_SESSION['acik_kilit_zamani']);
        girisLogla($LOG_DOSYASI, 'GIRIS', 'BASARILI', $clientIp, 'normal', $userAgentStr);
    } else {
        $_SESSION['acik_deneme'] = ($_SESSION['acik_deneme'] ?? 0) + 1;
        girisLogla($LOG_DOSYASI, 'GIRIS', 'BASARISIZ', $clientIp, 'yanlış parola', $userAgentStr);
        if ($_SESSION['acik_deneme'] >= $MAX_DENEME) {
            $_SESSION['acik_kilit_zamani'] = time();
            $kilitliMi = true;
            $kalanSure = $KILIT_SURESI;
            girisLogla($LOG_DOSYASI, 'KILIT', 'BASARISIZ', $clientIp, "$MAX_DENEME yanlış denemeden sonra kilitlendi", $userAgentStr);
        } else {
            $kalanDeneme = $MAX_DENEME - $_SESSION['acik_deneme'];
            $loginError = "Parola hatalı. Kalan deneme: $kalanDeneme";
        }
    }
}
if (isset($_GET['cikis'])) {
    if (!empty($_SESSION['acik_auth'])) {
        girisLogla($LOG_DOSYASI, 'CIKIS', 'BASARILI', $clientIp, 'çıkış yapıldı', $userAgentStr);
    }
    unset($_SESSION['acik_auth']);
    header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
    exit;
}

if (empty($_SESSION['acik_auth'])) {
?>
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Giriş - Güvenlik Denetimi</title>
<style>
  body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
    background:#0b0f1e; font-family:Consolas,'Courier New',monospace; color:#d8dee9; }
  form { background:#131a2e; border:1px solid #24304f; border-radius:10px; padding:32px; width:280px; max-width:90vw; }
  h1 { font-size:16px; color:#ff4d4d; margin:0 0 16px 0; text-align:center; }
  input { width:100%; padding:10px; border-radius:6px; border:1px solid #24304f; background:#0b0f1e;
    color:#d8dee9; margin-bottom:12px; box-sizing:border-box; font-family:inherit; }
  button { width:100%; padding:10px; border-radius:6px; border:none; background:#ff4d4d; color:#fff;
    font-weight:bold; cursor:pointer; font-family:inherit; }
  button:disabled { background:#5a3030; cursor:not-allowed; }
  .err { color:#ff6b6b; font-size:12px; margin-bottom:10px; text-align:center; }
  .info { color:#facc15; font-size:12px; margin-bottom:10px; text-align:center; }
</style>
</head>
<body>
  <form method="post">
    <h1>Güvenlik Denetim Paneli</h1>
    <?php if (!empty($timeoutMsg)): ?><div class="info">Hareketsizlik nedeniyle oturum sonlandi.</div><?php endif; ?>
    <?php if (!empty($loginError)): ?><div class="err"><?= h($loginError) ?></div><?php endif; ?>
    <?php if ($kilitliMi): ?>
      <div class="err">Çok fazla yanlış deneme. <?= h(ceil($kalanSure / 60)) ?> dakika sonra tekrar deneyin.</div>
      <input type="password" placeholder="Kilitli" disabled>
      <button type="submit" disabled>Giriş</button>
    <?php else: ?>
      <input type="hidden" name="csrf" value="<?= h($_SESSION['acik_csrf']) ?>">
      <input type="password" name="panel_password" placeholder="Parola" autofocus>
      <button type="submit">Giriş</button>
    <?php endif; ?>
  </form>
</body>
</html>
<?php
    exit;
}

/* ================= YARDIMCI FONKSIYONLAR ================= */

function run($cmd) {
    if (!function_exists('shell_exec')) return null;
    $out = @shell_exec($cmd . ' 2>&1');
    return $out !== null ? trim($out) : null;
}

function sonSatirlariOku($dosya, $maxSatir = 1000, $maxBayt = 2097152) {
    // Dosyada en yeni kayıt en basta oldugu için baştan okumak yeterli, ekstra seek/reverse gerekmez.
    if (!is_readable($dosya)) return [];
    $fp = fopen($dosya, 'r');
    if ($fp === false) return [];
    $içerik = fread($fp, $maxBayt);
    fclose($fp);
    $satirlar = array_values(array_filter(explode("\n", (string)$içerik), fn($s) => $s !== ''));
    return array_slice($satirlar, 0, $maxSatir);
}

/* ================= AJAX: GIRIS LOGU SAYFALAMA ================= */

if (isset($_GET['ajax']) && $_GET['ajax'] === 'log') {
    $sayfaBoyutu = 20;
    $tumSatirlar = sonSatirlariOku($LOG_DOSYASI, 5000); // dosyada zaten en yeni en basta
    $toplam = count($tumSatirlar);
    $toplamSayfa = max(1, (int)ceil($toplam / $sayfaBoyutu));
    $sayfa = max(1, min((int)($_GET['sayfa'] ?? 1), $toplamSayfa));
    $baslangic = ($sayfa - 1) * $sayfaBoyutu;
    $sayfaSatirlari = array_slice($tumSatirlar, $baslangic, $sayfaBoyutu);

    header('Content-Type: application/json; charset=utf-8');
    echo json_encode([
        'sayfa' => $sayfa,
        'toplamSayfa' => $toplamSayfa,
        'toplam' => $toplam,
        'satirlar' => array_map(function ($s) {
            $p = explode("\t", $s);
            return count($p) === 6 ? $p : [null, null, null, null, null, $s];
        }, $sayfaSatirlari),
    ], JSON_UNESCAPED_UNICODE);
    exit;
}

function olderThan($version, $limit) {
    if (!$version) return null;
    return version_compare($version, $limit, '<');
}

$isWindows = stripos(PHP_OS, 'WIN') === 0;
$docRoot = $_SERVER['DOCUMENT_ROOT'] ?: __DIR__;

/* ================= SÜRÜM TESPİTİ ================= */

$apacheRaw = $isWindows
    ? (run('httpd -v') ?: run('apachectl -v'))
    : (run('apache2 -v') ?: run('apachectl -v') ?: run('httpd -v'));
preg_match('/Apache\/([\d.]+)/i', (string)$apacheRaw, $m);
$apacheVersion = $m[1] ?? ($_SERVER['SERVER_SOFTWARE'] ?? null);

$nginxRaw = run('nginx -v');

$phpVersion = phpversion();
$opensslVersion = defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : null;
$curlVersion = function_exists('curl_version') ? (curl_version()['version'] ?? null) : null;

$mysqlRaw = run('mysql --version') ?: run('mysqld --version');
preg_match('/(?:mysql|mariadb)\s+Ver\s+([\d.]+)|(?:5\.\d+\.\d+|8\.\d+\.\d+|10\.\d+\.\d+)/i', (string)$mysqlRaw, $mm);
$mysqlVersion = $mm[1] ?? ($mm[0] ?? null);
$isMariaDb = stripos((string)$mysqlRaw, 'MariaDB') !== false;

/* ================= RISK PUANLAMA ================= */
$riskPoints = 0;
$maxPoints  = 0;

function inferKategori($bilesen) {
    $b = mb_strtolower($bilesen);
    $map = [
        'Ağ & HTTP'  => ['port','tls','http','cors','dns','spf','dmarc','server-status','dizin listeleme','xmlrpc','metod','ağ servisi','security.txt','başlığı'],
        'Dosya & Kod' => ['dosya','.git','backdoor','upload','vendor','node_modules','.htaccess','obfuscation','ifşa','.env','.ds_store','robots'],
        'Oturum'     => ['session','cookie'],
        'PHP Yapılandırma' => ['php ','ini','disable_functions','open_basedir','xdebug','ext/mysql','cgi.','enable_dl','assert','zend.','log_errors','html_errors','charset','max_input','short_open','register_argc','file_uploads','fonksiyonu','opcache'],
        'Sistem'     => ['root','yönetici','cron','görev','duvarı','cpu','ram','disk','kullanıcı'],
        'CMS / Uygulama' => ['cms','wordpress','phpmyadmin'],
    ];
    foreach ($map as $kat => $kelimeler) {
        foreach ($kelimeler as $kw) {
            if (mb_strpos($b, $kw) !== false) return $kat;
        }
    }
    return 'Diğer';
}

function addCheck(&$arr, &$riskPoints, &$maxPoints, $bilesen, $durum, $not, $weight = 1) {
    $maxPoints += $weight;
    if ($durum === 'RISKLI') $riskPoints += $weight;
    if ($durum === 'BILINMIYOR') $riskPoints += (int)round($weight / 2);
    $arr[] = ['bilesen' => $bilesen, 'durum' => $durum, 'not' => $not, 'kategori' => inferKategori($bilesen)];
}

$checks = [];

if ($phpVersion) {
    $eol = olderThan($phpVersion, '8.1.0');
    addCheck($checks, $riskPoints, $maxPoints, "PHP $phpVersion", $eol ? 'RISKLI' : 'IYI',
        $eol ? 'PHP 8.1 öncesi sürümler EOL olabilir; bilinen RCE/deserializasyon CVE\'lerini kontrol edin.'
             : 'Güncel bir majör seri. Küçük sürüm (patch) güncellemelerini takip edin.', 2);
}

if ($apacheVersion && preg_match('/^[\d.]+$/', $apacheVersion)) {
    $risky = olderThan($apacheVersion, '2.4.55');
    addCheck($checks, $riskPoints, $maxPoints, "Apache $apacheVersion", $risky ? 'RISKLI' : 'IYI',
        $risky ? 'Apache 2.4.55 öncesi sürümlerde bilinen CVE\'ler (mod_proxy, path traversal sınıfı) mevcuttur.'
               : 'Gorece güncel bir sürüm. CVE veritabanindan son yamalari teyit edin.', 2);
} elseif ($apacheRaw) {
    addCheck($checks, $riskPoints, $maxPoints, 'Apache', 'BILINMIYOR', 'Sürüm ayrıştırılamadı: ' . $apacheRaw, 1);
}

preg_match('/nginx\/([\d.]+)/i', (string)$nginxRaw, $nm);
$nv = $nm[1] ?? null;
if ($nv) {
    $risky = olderThan($nv, '1.25.0');
    addCheck($checks, $riskPoints, $maxPoints, "Nginx $nv", $risky ? 'RISKLI' : 'IYI',
        $risky ? 'Eski Nginx sürümü; bilinen CVE\'leri kontrol edin.' : 'Nginx tespit edildi ve sürümü güncel görünüyor.', 1);
} else {
    addCheck($checks, $riskPoints, $maxPoints, 'Nginx', 'BILGI',
        'Nginx tespit edilmedi (yüklü değil ya da bu sunucuda Apache/baska bir web sunucusu kullaniliyor).', 0);
}

if ($mysqlVersion) {
    $limit = $isMariaDb ? '10.6.0' : '8.0.30';
    $risky = olderThan($mysqlVersion, $limit);
    addCheck($checks, $riskPoints, $maxPoints, ($isMariaDb ? 'MariaDB ' : 'mysql-server ') . $mysqlVersion, $risky ? 'RISKLI' : 'IYI',
        $risky ? 'Eski sürüm; bilinen ayricalik yükseltme / DoS CVE\'lerini kontrol edin.'
               : 'Gorece güncel. Son güvenlik bultenlerini kontrol edin.', 2);
}

if ($opensslVersion) {
    $risky = (bool)preg_match('/OpenSSL 1\.0/i', $opensslVersion);
    addCheck($checks, $riskPoints, $maxPoints, $opensslVersion, $risky ? 'RISKLI' : 'IYI',
        $risky ? 'OpenSSL 1.0.x serisi EOL; TLS ile ilgili bilinen açıklar icerebilir.' : 'Güncel seri.', 1);
}

if ($curlVersion) {
    addCheck($checks, $riskPoints, $maxPoints, "cURL $curlVersion", 'BILGI', 'Sürümü güncel tutun, cURL CVE\'leri düzenli yayinlanir.', 0);
}

/* ================= EK SERVIS SÜRÜMLERİ (Redis, PostgreSQL, Memcached, ImageMagick) ================= */

$redisRaw = run('redis-server --version') ?: run('redis-cli --version');
if ($redisRaw && preg_match('/v=?(\d+\.\d+(\.\d+)?)/i', $redisRaw, $rdm)) {
    $risky = olderThan($rdm[1], '7.0.0');
    addCheck($checks, $riskPoints, $maxPoints, "Redis {$rdm[1]}", $risky ? 'RISKLI' : 'IYI',
        $risky ? 'Eski Redis sürümü; kimliksiz erisimde veri sızıntısı/RCE zincirlerine karşı güncelleyin, ayrica dinleme adresini/parolasini kontrol edin.'
               : 'Gorece güncel. Yine de authentication (requirepass) ve bind adresini kontrol edin.', 2);
}

$pgRaw = run('psql --version') ?: run('postgres --version');
if ($pgRaw && preg_match('/(\d+\.\d+)/', $pgRaw, $pgm)) {
    $risky = olderThan($pgm[1], '14.0');
    addCheck($checks, $riskPoints, $maxPoints, "PostgreSQL {$pgm[1]}", $risky ? 'RISKLI' : 'IYI',
        $risky ? 'Eski PostgreSQL sürümü olabilir; bilinen CVE\'leri kontrol edip güncelleyin.'
               : 'Gorece güncel. Son güvenlik bultenlerini kontrol edin.', 2);
}

$memcachedRaw = run('memcached -h 2>&1 | head -1') ?: run('memcached --version');
if ($memcachedRaw && preg_match('/(\d+\.\d+\.\d+)/', $memcachedRaw, $mcm)) {
    addCheck($checks, $riskPoints, $maxPoints, "Memcached {$mcm[1]}", 'BILGI',
        'Memcached tespit edildi - kimliksiz/herkese açık dinliyorsa veri sızıntısı ve amplifikasyon DDoS riski taşır, bind-address ve firewall\'u kontrol edin.', 1);
}

$imagickRaw = run('convert --version') ?: run('magick --version');
if ($imagickRaw && preg_match('/ImageMagick (\d+\.\d+\.\d+)/i', $imagickRaw, $imm)) {
    $risky = olderThan($imm[1], '6.9.12') || olderThan($imm[1], '7.1.0');
    addCheck($checks, $riskPoints, $maxPoints, "ImageMagick {$imm[1]}", 'DIKKAT',
        'ImageMagick tespit edildi - geçmişte "ImageTragick" (CVE-2016-3714) gibi kritik RCE zafiyetleri barındırdı. policy.xml ile riskli coder\'lari (MSL, EPHEMERAL, URL, HTTPS) devre dışı biraktiginizdan ve surumun güncel oldugundan emin olun.', 2);
}

/* ================= KONFIGURASYON KONTROLLERİ ================= */

$configChecks = [];

addCheck($configChecks, $riskPoints, $maxPoints, 'expose_php',
    ini_get('expose_php') ? 'RISKLI' : 'IYI',
    'Açık ise PHP sürümü HTTP basliklarinda ifşa olur (php.ini -> expose_php = Off).', 1);

addCheck($configChecks, $riskPoints, $maxPoints, 'display_errors',
    ini_get('display_errors') ? 'RISKLI' : 'IYI',
    'Üretimde açık kalirsa hata mesajlari yol/kod bilgisi sızdırabilir.', 2);

addCheck($configChecks, $riskPoints, $maxPoints, 'allow_url_fopen',
    ini_get('allow_url_fopen') ? 'DIKKAT' : 'IYI',
    'Açık ise uzak dosya include/require (RFI) riski artabilir.', 1);

addCheck($configChecks, $riskPoints, $maxPoints, 'allow_url_include',
    ini_get('allow_url_include') ? 'RISKLI' : 'IYI',
    'Açık olması çok yüksek RFI riski demektir; kapalı olmalı.', 2);

foreach (['exec','shell_exec','system','passthru','proc_open','popen'] as $tfn) {
    $tfnAcik = function_exists($tfn);
    addCheck($configChecks, $riskPoints, $maxPoints, "PHP fonksiyonu: {$tfn}()",
        $tfnAcik ? 'DIKKAT' : 'IYI',
        $tfnAcik ? "{$tfn}() aktif; bir RCE zafiyeti varsa etkisini büyütür. Gerekmiyorsa disable_functions ile kapatin."
                 : "{$tfn}() devre dışı/mevcut değil.", 1);
}

addCheck($configChecks, $riskPoints, $maxPoints, 'session.cookie_httponly',
    ini_get('session.cookie_httponly') ? 'IYI' : 'DIKKAT',
    'Kapalı ise oturum çerezleri XSS üzerinden JS ile okunabilir.', 1);

addCheck($configChecks, $riskPoints, $maxPoints, 'session.cookie_secure',
    ini_get('session.cookie_secure') ? 'IYI' : 'DIKKAT',
    'Kapalı ise oturum çerezleri HTTPS dışında da gönderilebilir.', 1);

/* ================= DOSYA IZINLERI (world-writable) ================= */

if (!$isWindows) {
    $writableTargets = [$docRoot, __DIR__];
    $writableFound = [];
    foreach (array_unique($writableTargets) as $t) {
        if (is_dir($t) && (fileperms($t) & 0002)) $writableFound[] = $t;
    }
    addCheck($configChecks, $riskPoints, $maxPoints, 'Herkes tarafından yazılabilir dizin kontrolü',
        $writableFound ? 'RISKLI' : 'IYI',
        $writableFound ? 'Su dizinler diğer kullanıcılar tarafından yazılabilir: ' . implode(', ', $writableFound) . ' - izinleri chmod ile sıkılaştırın.'
                       : 'Belge kökü/betik dizini baska kullanıcılarca yazılabilir değil.', 1);
}

/* ================= .GIT DİZİNİ TAM İFŞA KONTROLÜ ================= */

$gitDir = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . '.git';
$gitExposed = is_dir($gitDir) && is_readable($gitDir . DIRECTORY_SEPARATOR . 'HEAD');
addCheck($configChecks, $riskPoints, $maxPoints, '.git dizini ifşası',
    $gitExposed ? 'RISKLI' : 'IYI',
    $gitExposed ? 'Belge kökünde okunabilir .git dizini var - bütün kaynak kodu/geçmişi indirilebilir olabilir. Web sunucusundan erişimi engelleyin veya dizini taşıyın.'
                : '.git dizini belge kökünde bulunamadı / okunamıyor.', 3);

/* ================= ŞÜPHELİ / BACKDOOR DOSYA TARAMASI ================= */

// Belge kökünde (sadece 1. seviye) PHP dosyalarinda bilinen şüpheli imzalari tara (ağır olmasın diye sınırlı)
$foundSignatures = [];
if (is_dir($docRoot) && is_readable($docRoot)) {
    $taranan = 0;
    foreach (glob(rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . '*.php') as $f) {
        if ($taranan++ >= 200) break; // asiri büyük dizinlerde zaman asimini onle
        $içerik = @file_get_contents($f, false, null, 0, 4000);
        if ($içerik === false) continue;
        if (preg_match('/eval\s*\(\s*(base64_decode|gzinflate|str_rot13)\s*\(/i', $içerik) ||
            preg_match('/(FilesMan|c99shell|r57shell|WSO\s*\d)/i', $içerik)) {
            $foundSignatures[] = basename($f);
        }
    }
}
addCheck($configChecks, $riskPoints, $maxPoints, 'Obfuscation imza taramasi (eval/base64, belge kökü)',
    $foundSignatures ? 'RISKLI' : 'IYI',
    $foundSignatures
        ? 'Şüpheli içerikli dosyalar: ' . implode(', ', $foundSignatures) . ' - inceleyip şüpheliyse hemen silin/karantinaya alin.'
        : 'Bilinen obfuscation imzasi bulunamadı (yalnızca 1. seviye, 200 dosya ile sınırlı tarama).', 3);


/* ================= PHPMYADMIN DETAYLI TESPIT VE BILGI ================= */

$pmaAdaylari = ['phpmyadmin', 'phpMyAdmin', 'pma', 'dbadmin', 'myadmin'];
$pmaYolu = null;
foreach ($pmaAdaylari as $pa) {
    $tam = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . $pa;
    if (is_dir($tam)) { $pmaYolu = $pa; break; }
}
if ($pmaYolu) {
    $pmaTamYol = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . $pmaYolu;
    $pmaSurum = null;
    $versionJs = $pmaTamYol . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'version.js';
    $changelog = $pmaTamYol . DIRECTORY_SEPARATOR . 'ChangeLog';
    $composerJson = $pmaTamYol . DIRECTORY_SEPARATOR . 'composer.json';
    if (is_readable($versionJs) && preg_match('/PMA_VERSION\s*=\s*"([\d.]+)"/', file_get_contents($versionJs), $vm)) {
        $pmaSurum = $vm[1];
    } elseif (is_readable($composerJson) && preg_match('/"version"\s*:\s*"([\d.]+)"/', file_get_contents($composerJson), $vm)) {
        $pmaSurum = $vm[1];
    } elseif (is_readable($changelog) && preg_match('/([\d]+\.[\d]+\.[\d]+)/', file_get_contents($changelog, false, null, 0, 500), $vm)) {
        $pmaSurum = $vm[1];
    }
    $pmaEski = $pmaSurum ? olderThan($pmaSurum, '5.2.0') : null;
    $pmaConfigVar = file_exists($pmaTamYol . DIRECTORY_SEPARATOR . 'config.inc.php');
    $pmaSetupVar = is_dir($pmaTamYol . DIRECTORY_SEPARATOR . 'setup');

    $pmaNot = 'phpMyAdmin, veritabanina doğrudan tam erişim sağladığı için sunuculardaki en çok hedef alinan panellerden biridir. ';
    $pmaNot .= $pmaSurum ? "Tespit edilen sürüm: $pmaSurum. " : 'Sürüm dosyadan okunamadı (sürümü manuel kontrol edin). ';
    if ($pmaEski) $pmaNot .= 'Bu sürüm eski olabilir, bilinen CVE\'leri kontrol edip güncelleyin. ';
    if ($pmaSetupVar) $pmaNot .= 'setup/ dizini hala mevcut - kurulumdan sonra bu dizini silin, aksi halde konfigürasyon değiştirilebilir. ';
    $pmaNot .= 'Öneriler: (1) yolu varsayılan "phpmyadmin" yerine tahmin edilmesi zor bir isimle değiştirin, (2) sadece belirli IP\'lerden erişime izin verin (.htaccess/firewall), (3) MySQL root ile doğrudan giriş yapmayı engelleyin (AllowNoPassword=false, root için ayrı kısıtlama), (4) mümkünse önüne ek bir HTTP Basic Auth katmanı koyun.';

    addCheck($configChecks, $riskPoints, $maxPoints, "phpMyAdmin tespiti ($pmaYolu/)",
        $pmaEski ? 'RISKLI' : 'DIKKAT',
        $pmaNot, $pmaEski ? 3 : 2);
    addCheck($checks, $riskPoints, $maxPoints, 'phpMyAdmin ' . ($pmaSurum ?: '(sürüm bilinmiyor)'),
        $pmaEski ? 'RISKLI' : ($pmaSurum ? 'IYI' : 'BILINMIYOR'),
        $pmaSurum
            ? ($pmaEski ? 'Eski sürüm olabilir, bilinen CVE\'leri kontrol edip güncelleyin.' : 'Gorece güncel görünüyor, yine de erişimi IP/parola ile kisitlamayi unutmayin.')
            : 'Dizin bulundu ama sürüm dosyadan okunamadı; manuel kontrol edin.', 2);
} else {
    addCheck($configChecks, $riskPoints, $maxPoints, 'phpMyAdmin tespiti',
        'IYI',
        'Belge kökünde bilinen bir phpMyAdmin dizini (phpmyadmin, pma, dbadmin, myadmin) bulunamadı.', 0);
}

/* ================= AÇIK AG SERVİSLERİ (DB/CACHE PORTLARI) ================= */

$netstatCmd = $isWindows ? 'netstat -an' : '(ss -tulnp 2>/dev/null || netstat -tulnp 2>/dev/null)';
$netstatOut = run($netstatCmd);
$riskliPortlar = ['3306' => 'MySQL/MariaDB', '5432' => 'PostgreSQL', '6379' => 'Redis',
    '27017' => 'MongoDB', '11211' => 'Memcached', '9200' => 'Elasticsearch'];
foreach ($riskliPortlar as $port => $ad) {
    $portAcik = $netstatOut && (preg_match('/(0\.0\.0\.0|\*|\[::\]):' . $port . '\b/', $netstatOut) ||
        preg_match('/(0\.0\.0\.0|\*):' . $port . '\s/', $netstatOut));
    addCheck($configChecks, $riskPoints, $maxPoints, "Ag servisi: $ad ($port)",
        $portAcik ? 'RISKLI' : ($netstatOut ? 'IYI' : 'BILINMIYOR'),
        $portAcik
            ? "$ad, 0.0.0.0 üzerinde herkese açık dinliyor - bind-address'i 127.0.0.1 yapin veya firewall ile kısıtlayın."
            : ($netstatOut ? "$ad portu ($port) sadece localhost'a bağlı görünüyor." : 'netstat/ss çalıştırılamadı, kontrol edilemedi.'),
        1);
}

/* ================= server-status / server-info İFŞASI (self-check) ================= */

$statusExposed = null;
if (!empty($_SERVER['HTTP_HOST']) && function_exists('curl_init')) {
    $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    $selfUrl = $scheme . '://' . $_SERVER['HTTP_HOST'] . '/server-status';
    $ch = curl_init($selfUrl);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3, CURLOPT_NOBODY => false]);
    $body = @curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    $statusExposed = ($code === 200 && $body && stripos($body, 'Apache Server Status') !== false);
}
if ($statusExposed !== null) {
    addCheck($configChecks, $riskPoints, $maxPoints, '/server-status ifşası',
        $statusExposed ? 'RISKLI' : 'IYI',
        $statusExposed ? 'mod_status herkese açık yayinlaniyor; aktif istekler/IP\'ler ifşa oluyor. httpd.conf\'ta Require local ekleyin.'
                       : '/server-status disaridan erişilebilir/yayinda değil.', 1);
}

/* ================= SSL SERTIFIKA SÜRESİ ================= */

if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' && !empty($_SERVER['HTTP_HOST'])) {
    $host = preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
    $ctx = stream_context_create(['ssl' => ['capture_peer_cert' => true, 'verify_peer' => false, 'verify_peer_name' => false]]);
    $fp = @stream_socket_client("ssl://$host:443", $errno, $errstr, 3, STREAM_CLIENT_CONNECT, $ctx);
    if ($fp) {
        $params = stream_context_get_params($fp);
        fclose($fp);
        if (!empty($params['options']['ssl']['peer_certificate'])) {
            $certInfo = openssl_x509_parse($params['options']['ssl']['peer_certificate']);
            $kalanGun = isset($certInfo['validTo_time_t']) ? floor(($certInfo['validTo_time_t'] - time()) / 86400) : null;
            if ($kalanGun !== null) {
                $risky = $kalanGun < 14;
                addCheck($configChecks, $riskPoints, $maxPoints, 'SSL sertifika süresi',
                    $risky ? 'RISKLI' : 'IYI',
                    $risky ? "Sertifika $kalanGun gün içinde doluyor - yenileyin (ornegin certbot renew)."
                           : "Sertifika geçerliliği $kalanGun gün kaldi.", 2);
            }
        }
    }
}

/* ================= disable_functions / open_basedir BİLGİSİ ================= */

addCheck($configChecks, $riskPoints, $maxPoints, 'disable_functions',
    ini_get('disable_functions') ? 'IYI' : 'DIKKAT',
    ini_get('disable_functions') ? 'Tanımlı: ' . ini_get('disable_functions') : 'Hiçbir fonksiyon devre dışı bırakılmamış; exec/shell_exec gibi fonksiyonlari değerlendirin.', 0);

addCheck($configChecks, $riskPoints, $maxPoints, 'open_basedir',
    ini_get('open_basedir') ? 'IYI' : 'DIKKAT',
    ini_get('open_basedir') ? 'Tanımlı: ' . ini_get('open_basedir') : 'Tanımlı değil; PHP dosya sistemi genelinde okuma/yazma yapabilir (path traversal etkisini büyütür).', 1);

/* ================= XDEBUG / RISKLI PHP EKLENTİLERİ ================= */

$yüklü = get_loaded_extensions();
$xdebugAcik = in_array('xdebug', array_map('strtolower', $yüklü), true);
addCheck($configChecks, $riskPoints, $maxPoints, 'Xdebug eklentisi',
    $xdebugAcik ? 'RISKLI' : 'IYI',
    $xdebugAcik ? 'Xdebug yüklü; üretimde açık kalirsa bilgi ifşası ve bazi sürümlerde RCE zincirlerinde kullanılabilir. Sadece geliştirme ortaminda açık olmalı.'
                : 'Xdebug yüklü değil.', 2);

$eskiMysqlExt = in_array('mysql', array_map('strtolower', $yüklü), true);
addCheck($configChecks, $riskPoints, $maxPoints, 'Eski ext/mysql eklentisi',
    $eskiMysqlExt ? 'RISKLI' : 'IYI',
    $eskiMysqlExt ? 'Eski ve kaldırılmış "mysql" eklentisi (mysqli/PDO değil) yüklü; PHP 7+ ile birlikte kaldirilmisti, guvensiz kabul edilir.'
                  : 'Eski/kaldırılmış mysql eklentisi bulunamadı.', 1);

/* ================= SON DEĞİŞEN DOSYALAR (IOC) ================= */

$sonDegisenler = [];
if (is_dir($docRoot) && is_readable($docRoot)) {
    $esik = time() - (2 * 86400); // son 48 saat
    $taranan2 = 0;
    foreach (glob(rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . '*') as $f) {
        if ($taranan2++ >= 500) break;
        if (is_file($f) && filemtime($f) > $esik) {
            $sonDegisenler[] = basename($f) . ' (' . date('d.m H:i', filemtime($f)) . ')';
        }
    }
}
addCheck($configChecks, $riskPoints, $maxPoints, 'Son 48 saatte değişen dosyalar (belge kökü, 1. seviye)',
    $sonDegisenler ? 'DIKKAT' : 'IYI',
    $sonDegisenler
        ? 'Yakin zamanda değişen: ' . implode(', ', array_slice($sonDegisenler, 0, 15)) . (count($sonDegisenler) > 15 ? ' ... (+' . (count($sonDegisenler) - 15) . ')' : '') . ' - bu siz mi yaptiniz yoksa beklenmeyen bir değişiklik mi kontrol edin.'
        : 'Belge kökünde son 48 saatte değişmiş dosya bulunamadı.', 1);

/* ================= ZAMANLANMIS GOREVLER / CRON (KALICILIK KONTROLÜ) ================= */

$zamanlanmisGorevler = $isWindows
    ? run('schtasks /query /fo LIST 2>&1 | findstr /B "TaskName"')
    : run('crontab -l 2>&1');
$cronVarMi = $zamanlanmisGorevler && stripos($zamanlanmisGorevler, 'no crontab') === false && stripos($zamanlanmisGorevler, 'bulunamadı') === false;
addCheck($configChecks, $riskPoints, $maxPoints, $isWindows ? 'Zamanlanmış görevler (mevcut kullanıcı)' : 'Cron görevleri (mevcut kullanıcı)',
    $cronVarMi ? 'DIKKAT' : 'IYI',
    $cronVarMi ? 'Tanımlı görevler bulundu, elle inceleyin (beklenmeyen indirme/çalıştırma komutları kalıcılık/backdoor işareti olabilir): ' . substr($zamanlanmisGorevler, 0, 300)
               : 'Mevcut kullanıcı için tanımlı zamanlanmış görev/cron bulunamadı (yalnızca bu PHP\'nin çalıştığı kullanıcı görülebilir).', 1);

/* ================= CMS TESPİTİ VE SÜRÜM İFŞASI ================= */

$cmsBulunan = [];
$wpVersionFile = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'wp-includes' . DIRECTORY_SEPARATOR . 'version.php';
if (is_readable($wpVersionFile)) {
    $içerik = file_get_contents($wpVersionFile);
    if (preg_match('/\$wp_version\s*=\s*[\'"]([^\'"]+)[\'"]/', $içerik, $wm)) {
        $cmsBulunan[] = 'WordPress ' . $wm[1];
    }
}
$joomlaFile = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Version.php';
if (is_readable($joomlaFile)) $cmsBulunan[] = 'Joomla (Version.php mevcut)';
$drupalFile = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'core' . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'Drupal.php';
if (is_readable($drupalFile)) {
    $içerik = file_get_contents($drupalFile);
    if (preg_match('/const\s+VERSION\s*=\s*[\'"]([^\'"]+)[\'"]/', $içerik, $dm)) {
        $cmsBulunan[] = 'Drupal ' . $dm[1];
    }
}
addCheck($configChecks, $riskPoints, $maxPoints, 'CMS tespiti ve sürüm ifşası',
    $cmsBulunan ? 'DIKKAT' : 'IYI',
    $cmsBulunan ? 'Tespit edilen: ' . implode(', ', $cmsBulunan) . ' - sürüm bilgisi saldırgana hangi CVE\'leri deneyecegini soyler, CMS ve eklentileri güncel tutun.'
                : 'Bilinen bir CMS imzasi (WordPress/Joomla/Drupal) tespit edilemedi.', 1);

/* ================= GÜVENLİK DUVARI DURUMU ================= */

$firewallOut = $isWindows
    ? run('netsh advfirewall show allprofiles state')
    : (run('ufw status 2>&1') ?: run('firewall-cmd --state 2>&1'));
$firewallAktif = $firewallOut && (stripos($firewallOut, 'ON') !== false || stripos($firewallOut, 'active') !== false || stripos($firewallOut, 'running') !== false);
addCheck($configChecks, $riskPoints, $maxPoints, 'Güvenlik duvari durumu',
    $firewallOut ? ($firewallAktif ? 'IYI' : 'RISKLI') : 'BILINMIYOR',
    $firewallOut ? ($firewallAktif ? 'Güvenlik duvari aktif görünüyor.' : 'Güvenlik duvari kapalı/pasif görünüyor - internete açık bir sunucuda risklidir.')
                 : 'Güvenlik duvari durumu bu yetkiyle tespit edilemedi.', 1);

/* ================= SİSTEM YÖNETİCİ HESAPLARI ================= */

$yöneticiler = $isWindows
    ? run('net localgroup administrators')
    : run('awk -F: \'$3==0 || ($3>=1000 && $7 !~ /nologin|false/) {print $1}\' /etc/passwd 2>&1');
addCheck($configChecks, $riskPoints, $maxPoints, $isWindows ? 'Yerel yönetici hesaplari' : 'Root/interaktif kabuklu kullanıcılar',
    'BILGI',
    $yöneticiler ? 'Tespit edilen: ' . preg_replace('/\s+/', ' ', substr($yöneticiler, 0, 400)) . ' - tanımadığınız bir hesap varsa hemen inceleyin.'
                 : 'Tespit edilemedi (yetki kısıtlaması olabilir).', 0);

/* ================= PHP ROOT OLARAK ÇALIŞIYOR MU ================= */

if (!$isWindows && function_exists('posix_getuid')) {
    $rootMu = posix_getuid() === 0;
    addCheck($configChecks, $riskPoints, $maxPoints, 'PHP çalışan kullanıcı',
        $rootMu ? 'RISKLI' : 'IYI',
        $rootMu ? 'PHP/web sunucusu root olarak çalışıyor - bir RCE zafiyeti doğrudan tam sistem ele geçirilmesine yol acar. Web sunucusunu ayricaliksiz bir kullanıcı (www-data vb.) ile çalıştırın.'
                : 'PHP root dışında bir kullanıcı ile çalışıyor.', 3);
}

/* ================= disable_functions TAMLIK KONTROLÜ ================= */

$onerilenDisable = ['exec','shell_exec','system','passthru','proc_open','popen','show_source','pcntl_exec'];
$mevcutDisable = array_map('trim', explode(',', (string)ini_get('disable_functions')));
$eksikDisable = array_filter($onerilenDisable, fn($f) => function_exists($f) && !in_array($f, $mevcutDisable, true));
addCheck($configChecks, $riskPoints, $maxPoints, 'disable_functions tamlik kontrolü',
    $eksikDisable ? 'DIKKAT' : 'IYI',
    $eksikDisable ? 'Genellikle gerekmeyen ama hala aktif olan fonksiyonlar: ' . implode(', ', $eksikDisable) . ' - uygulamanız kullanmıyorsa disable_functions listesine ekleyin.'
                  : 'Önerilen kritik fonksiyonlarin tümü zaten devre dışı veya mevcut değil.', 1);

/* ================= .HTACCESS ŞÜPHELİ ICERIK TARAMASI ================= */

$htaccessPath = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . '.htaccess';
$htaccessSupheli = [];
if (is_readable($htaccessPath)) {
    $icerikHt = file_get_contents($htaccessPath);
    if (preg_match('/RewriteRule[^\n]*\b(base64|eval)\b/i', $icerikHt)) {
        $htaccessSupheli[] = 'şüpheli RewriteRule (base64/eval içerikli)';
    }
    if (preg_match_all('/RewriteRule[^\n]*https?:\/\/([a-z0-9.-]+)/i', $icerikHt, $rmAll)) {
        $ownHost = strtolower(preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST'] ?? ''));
        foreach ($rmAll[1] as $hedefHost) {
            if ($ownHost && strtolower($hedefHost) !== $ownHost) {
                $htaccessSupheli[] = "farkli bir domaine yönlendirme: $hedefHost";
                break;
            }
        }
    }
    if (preg_match('/php_value\s+auto_prepend_file|php_value\s+auto_append_file/i', $icerikHt)) {
        $htaccessSupheli[] = 'auto_prepend_file/auto_append_file (gizli kod çalıştırma vektoru)';
    }
}
$htStatus = $htaccessSupheli ? 'RISKLI' : (is_readable($htaccessPath) ? 'IYI' : 'BILGI');
addCheck($configChecks, $riskPoints, $maxPoints, '.htaccess şüpheli içerik taramasi', $htStatus,
    $htaccessSupheli ? 'Bulundu: ' . implode('; ', $htaccessSupheli) . ' - bu genellikle bir sitenin hacklenip spam/malware yönlendirmesi için kullanildiginin isaretidir.'
                      : (is_readable($htaccessPath) ? '.htaccess okundu, bilinen şüpheli bir orunty bulunamadı.' : '.htaccess bulunamadı/okunamadı.'), 2);

/* ================= KAYNAK LIMITLERI (BILGI) ================= */

addCheck($configChecks, $riskPoints, $maxPoints, 'Kaynak limitleri',
    'BILGI',
    sprintf('max_execution_time=%s, memory_limit=%s, upload_max_filesize=%s, post_max_size=%s',
        ini_get('max_execution_time'), ini_get('memory_limit'), ini_get('upload_max_filesize'), ini_get('post_max_size')), 0);

/* ================= PHP HATA LOGU (SON SATIRLAR) ================= */

$phpErrorLog = ini_get('error_log');
$errorLogTail = [];
if ($phpErrorLog && is_readable($phpErrorLog) && filesize($phpErrorLog) < 20 * 1024 * 1024) {
    $lines = @file($phpErrorLog, FILE_IGNORE_NEW_LINES);
    if ($lines) $errorLogTail = array_slice($lines, -5);
}
addCheck($configChecks, $riskPoints, $maxPoints, 'PHP hata logu',
    'BILGI',
    $errorLogTail ? 'Son satirlar (' . basename((string)$phpErrorLog) . '): ' . implode(' | ', array_map(fn($l) => mb_substr($l, 0, 150), $errorLogTail))
                  : ($phpErrorLog ? 'Log dosyası okunamadı/bos: ' . $phpErrorLog : 'error_log yolu tanımlı değil (muhtemelen sunucu genel logunu kullaniyor).'), 0);

/* ================= HTTP GÜVENLİK BAŞLIKLARI ================= */

$expectedHeaders = [
    'Strict-Transport-Security' => 'HTTPS zorunlu kilma (HSTS) eksik.',
    'X-Content-Type-Options'    => 'MIME sniffing koruması eksik.',
    'X-Frame-Options'           => 'Clickjacking koruması eksik.',
    'Content-Security-Policy'   => 'XSS/veri enjeksiyonuna karşı CSP eksik.',
    'Referrer-Policy'           => 'Baska sitelere geçerken URL/referrer bilgisi fazla sızabilir.',
    'Permissions-Policy'        => 'Tarayıcı API\'lerinin (kamera, konum, mikrofon vb.) kullanimini kisitlayan baslik eksik.',
    'Cross-Origin-Opener-Policy'   => 'Sayfa izolasyonu (Spectre sınıfı saldırılar) için COOP eksik.',
    'Cross-Origin-Resource-Policy' => 'Kaynaklarin baska sitelerce yuklenmesini kisitlayan CORP eksik.',
];
$sentHeaders = function_exists('headers_list') ? headers_list() : [];
$sentHeaderNames = array_map(function ($h) { return strtolower(explode(':', $h)[0]); }, $sentHeaders);
foreach ($expectedHeaders as $hdr => $desc) {
    $hdrVar = in_array(strtolower($hdr), $sentHeaderNames, true);
    addCheck($configChecks, $riskPoints, $maxPoints, "HTTP basligi: $hdr",
        $hdrVar ? 'IYI' : 'DIKKAT',
        $hdrVar ? "$hdr gönderiliyor." : "$hdr eksik. $desc Apache/Nginx veya PHP header() ile ekleyin.", 1);
}

/* ================= SELF-REQUEST YARDIMCISI (kendi sunucusuna güvenli GET/OPTIONS) ================= */

function selfIste($path, $method = 'GET', $https = null) {
    if (empty($_SERVER['HTTP_HOST']) || !function_exists('curl_init')) return null;
    $scheme = $https === null
        ? ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http')
        : ($https ? 'https' : 'http');
    $url = $scheme . '://' . $_SERVER['HTTP_HOST'] . $path;
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => true,
        CURLOPT_TIMEOUT => 3,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
    ]);
    $resp = @curl_exec($ch);
    if ($resp === false) { curl_close($ch); return null; }
    $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    return ['code' => $code, 'headers' => substr($resp, 0, $headerSize), 'body' => substr($resp, $headerSize)];
}

/* ================= DIZIN LISTELEME (Options Indexes) ================= */

$dirListResp = selfIste('/');
$dirListingAcik = $dirListResp && $dirListResp['code'] === 200 &&
    (preg_match('/Index of \//i', $dirListResp['body']) || preg_match('/<title>Index of/i', $dirListResp['body']));
addCheck($configChecks, $riskPoints, $maxPoints, 'Dizin listeleme (Options Indexes)',
    $dirListingAcik ? 'RISKLI' : ($dirListResp ? 'IYI' : 'BILGI'),
    $dirListingAcik ? 'Belge kökünde index dosyası olmadan dizin içeriği listeleniyor - dosya yapısını saldırgana gösterir. Apache\'de "Options -Indexes" ekleyin.'
                    : ($dirListResp ? 'Dizin listeleme kapalı görünüyor (index dosyası var ya da Indexes kapalı).' : 'Self-request yapılamadı (curl yok/erişim sınırlı).'), 1);

/* ================= HTTP -> HTTPS ZORLAMASI ================= */

$httpResp = selfIste('/', 'GET', false);
if ($httpResp) {
    $yonlendiriyorMu = in_array($httpResp['code'], [301, 302, 307, 308], true) && stripos($httpResp['headers'], 'https://') !== false;
    addCheck($configChecks, $riskPoints, $maxPoints, 'HTTP -> HTTPS zorlamasi',
        $yonlendiriyorMu ? 'IYI' : 'DIKKAT',
        $yonlendiriyorMu ? 'Duz HTTP istekleri otomatik olarak HTTPS\'e yönlendiriliyor.'
                         : 'Duz HTTP (80 port) üzerinden site şifresiz erisilebiliyor olabilir - trafik/oturum bilgisi ortadaki adam saldırısına açık olabilir. 301 yönlendirme + HSTS ekleyin.', 2);
}

/* ================= TEHLIKELI HTTP METODLARI (TRACE/PUT/DELETE) ================= */

$optionsResp = selfIste('/', 'OPTIONS');
if ($optionsResp) {
    preg_match('/Allow:\s*([^\r\n]+)/i', $optionsResp['headers'], $am);
    $allowList = $am[1] ?? '';
    $riskyMethods = array_filter(['TRACE', 'PUT', 'DELETE'], fn($mth) => stripos($allowList, $mth) !== false);
    addCheck($configChecks, $riskPoints, $maxPoints, 'Tehlikeli HTTP metodlari',
        $riskyMethods ? 'DIKKAT' : 'IYI',
        $riskyMethods ? 'Açık metodlar: ' . implode(', ', $riskyMethods) . ' - TRACE XST saldırısına, PUT/DELETE dosya yükleme/silmeye izin verebilir. Web sunucusunda sınırlandırın.'
                      : ($allowList ? "Bildirilen metodlar güvenli görünüyor: $allowList" : 'OPTIONS yaniti alinamadi/Allow basligi yok.'), 1);
}

/* ================= CORS YANLIS KONFİGÜRASYONU ================= */

if ($dirListResp) {
    preg_match('/Access-Control-Allow-Origin:\s*([^\r\n]+)/i', $dirListResp['headers'], $cm);
    $acao = trim($cm[1] ?? '');
    $credsAcik = stripos($dirListResp['headers'], 'Access-Control-Allow-Credentials: true') !== false;
    $corsRisky = $acao === '*' && $credsAcik;
    if ($acao !== '') {
        addCheck($configChecks, $riskPoints, $maxPoints, 'CORS konfigürasyonunu',
            $corsRisky ? 'RISKLI' : 'BILGI',
            $corsRisky ? "Access-Control-Allow-Origin: * ile Allow-Credentials: true aynı anda açık - bu kombinasyon herhangi bir sitenin kimlik bilgilerinizle istek atmasına izin verebilir."
                       : "Tespit edilen ACAO değeri: $acao", 2);
    }
}

/* ================= X-POWERED-BY BAŞLIĞI İFŞASI ================= */

if ($dirListResp) {
    $xPoweredBy = null;
    if (preg_match('/X-Powered-By:\s*([^\r\n]+)/i', $dirListResp['headers'], $xpm)) $xPoweredBy = trim($xpm[1]);
    addCheck($configChecks, $riskPoints, $maxPoints, 'X-Powered-By basligi',
        $xPoweredBy ? 'DIKKAT' : 'IYI',
        $xPoweredBy ? "Gonderiliyor: \"$xPoweredBy\" - PHP/framework surumunu ifşa eder, saldırgana hangi CVE'leri deneyecegini soyler. php.ini'de expose_php = Off yapin veya web sunucusunda basligi kaldırın."
                    : 'X-Powered-By basligi gönderilmiyor.', 1);
}

/* ================= .DS_STORE SIZINTISI (MAC YUKLEMELERI) ================= */

$dsStoreVar = file_exists(rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . '.DS_Store');
addCheck($configChecks, $riskPoints, $maxPoints, '.DS_Store dosyası',
    $dsStoreVar ? 'DIKKAT' : 'IYI',
    $dsStoreVar ? '.DS_Store bulundu - macOS Finder\'in bırakıp gittigi bu dosya bazen dizin/dosya isimlerini ifşa eder. Silin ve .gitignore/deploy sürecinize ekleyin.'
                : '.DS_Store belge kökünde bulunamadı.', 1);

/* ================= ROBOTS.TXT HASSAS YOL İFŞASI ================= */

$robotsResp = selfIste('/robots.txt');
if ($robotsResp && $robotsResp['code'] === 200 && $robotsResp['body']) {
    preg_match_all('/Disallow:\s*([^\r\n]+)/i', $robotsResp['body'], $rmAll);
    $hassasYollar = array_filter($rmAll[1] ?? [], function ($y) {
        $y = trim($y);
        return $y !== '' && $y !== '/' && preg_match('/admin|login|wp-admin|backup|config|panel|dashboard|secret|private/i', $y);
    });
    addCheck($configChecks, $riskPoints, $maxPoints, 'robots.txt hassas yol ifşası',
        $hassasYollar ? 'DIKKAT' : 'IYI',
        $hassasYollar ? 'robots.txt içinde ilginc gorunen yollar var: ' . implode(', ', array_slice($hassasYollar, 0, 8)) . ' - arama motorlarini engellese de saldırganlara yol haritasi verir, ekstra erişim kontrolü (parola/IP) ile korumayi unutmayin.'
                      : 'robots.txt içinde belirgin şekilde hassas gorunen bir yol bulunamadı.', 1);
}

/* ================= VARSAYILAN "IT WORKS" / KURULUM SAYFASI ================= */

if ($dirListResp && $dirListResp['code'] === 200 && $dirListResp['body']) {
    $varsayilanSayfaMi = (bool)preg_match('/It works!|Welcome to nginx|Apache2 (?:Ubuntu|Debian) Default Page|IIS Windows Server/i', $dirListResp['body']);
    addCheck($configChecks, $riskPoints, $maxPoints, 'Varsayılan web sunucusu sayfasi',
        $varsayilanSayfaMi ? 'DIKKAT' : 'IYI',
        $varsayilanSayfaMi ? 'Ana sayfada hala web sunucusunun varsayılan karsilama sayfasi görünüyor - bu, sitenin düzgün deploy edilmedigini ya da eski/kullanilmayan bir vhost\'un hala açık oldugunu gosterebilir.'
                           : 'Varsayılan web sunucusu karsilama sayfasi tespit edilmedi.', 1);
}

/* ================= WORDPRESS KULLANICI NUMARALANDIRMA (REST API) ================= */

if ($cmsBulunan && stripos(implode(' ', $cmsBulunan), 'WordPress') !== false) {
    $wpUsersResp = selfIste('/wp-json/wp/v2/users');
    $wpEnumAcik = $wpUsersResp && $wpUsersResp['code'] === 200 && stripos($wpUsersResp['body'], '"slug"') !== false;
    addCheck($configChecks, $riskPoints, $maxPoints, 'WordPress REST API kullanıcı numaralandirma',
        $wpEnumAcik ? 'DIKKAT' : 'IYI',
        $wpEnumAcik ? '/wp-json/wp/v2/users herkese açık kullanıcı adlarini listeliyor - bu bilgi kaba kuvvet/oltalama saldirilarini kolaylastirir. Bir eklenti veya kod ile bu endpoint\'i kısıtlayın.'
                    : 'WordPress REST API kullanıcı listesi açık/erişilebilir değil.', 1);
}

/* ================= OPCACHE DURUMU ================= */

$opcacheAcik = extension_loaded('Zend OPcache') && ini_get('opcache.enable');
addCheck($configChecks, $riskPoints, $maxPoints, 'OPcache durumu',
    'BILGI',
    $opcacheAcik ? 'OPcache aktif - performans için iyi. opcache.validate_timestamps kapalıysa dosya degisikliklerinin yansimasi için cache temizlemeniz gerekebilir (güvenlik değil, operasyonel not).'
                 : 'OPcache aktif değil - güvenlik riski değil ama performans acisindan aktif edilmesi önerilir.', 0);

/* ================= ESKI/ZAYIF TLS SÜRÜMLERİ ================= */

if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' && !empty($_SERVER['HTTP_HOST'])) {
    $tlsHost = preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
    $tlsTest = [
        'TLS 1.0' => defined('STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT') ? STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT : null,
        'TLS 1.1' => defined('STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT') ? STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT : null,
    ];
    foreach ($tlsTest as $ad => $method) {
        if ($method === null) continue;
        $tlsAcik = false;
        $fp = @stream_socket_client("tcp://$tlsHost:443", $errno, $errstr, 3);
        if ($fp) {
            $ok = @stream_socket_enable_crypto($fp, true, $method);
            fclose($fp);
            $tlsAcik = $ok === true;
        }
        addCheck($configChecks, $riskPoints, $maxPoints, "$ad desteği",
            $tlsAcik ? 'RISKLI' : 'IYI',
            $tlsAcik ? "Sunucu hala $ad destekliyor - kırılgan/kaldırılmış kabul edilir, sadece TLS 1.2+ bırakın."
                     : "$ad kapalı görünüyor.", 2);
    }
}


/* ================= SESSION GÜVENLİK AYARLARI (EK) ================= */

addCheck($configChecks, $riskPoints, $maxPoints, 'session.cookie_samesite',
    in_array(strtolower((string)ini_get('session.cookie_samesite')), ['lax', 'strict'], true) ? 'IYI' : 'DIKKAT',
    in_array(strtolower((string)ini_get('session.cookie_samesite')), ['lax', 'strict'], true)
        ? 'Tanımlı: ' . ini_get('session.cookie_samesite')
        : 'SameSite tanımlı değil/None - CSRF riskini azaltmak için Lax veya Strict önerilir.', 1);

if (!$isWindows) {
    $sessionPath = session_save_path() ?: sys_get_temp_dir();
    $sessionPathParts = explode(';', $sessionPath);
    $realSessionPath = end($sessionPathParts) ?: $sessionPath;
    if (is_dir($realSessionPath)) {
        $worldReadable = (bool)(@fileperms($realSessionPath) & 0004);
        addCheck($configChecks, $riskPoints, $maxPoints, 'Session dosyaları dizin izni',
            $worldReadable ? 'DIKKAT' : 'IYI',
            $worldReadable ? "Session dizini ($realSessionPath) herkes tarafından okunabilir - paylaşımlı sunucuda diğer kullanıcılar oturum dosyalarınızı okuyabilir (session hijacking riski)."
                           : 'Session dizini diğerleri tarafından okunabilir görünmüyor.', 1);
    }
}

/* ================= .ENV DEBUG MODU KONTROLÜ ================= */

$envPath = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . '.env';
if (is_readable($envPath)) {
    $envIcerik = file_get_contents($envPath);
    $debugAcik = (bool)preg_match('/^\s*APP_DEBUG\s*=\s*true/im', $envIcerik);
    addCheck($configChecks, $riskPoints, $maxPoints, '.env APP_DEBUG modu',
        $debugAcik ? 'RISKLI' : 'IYI',
        $debugAcik ? 'APP_DEBUG=true - üretimde açık kalirsa hata sayfalari kaynak kod/ortam degiskenlerini (DB parolasi dahil) ifşa edebilir. Üretimde false yapin.'
                   : 'APP_DEBUG=true bulunamadı (ya da .env bu degiskeni içermiyor).', 2);
}

/* ================= WORDPRESS xmlrpc.php ERISILEBILIRLIGI ================= */

if ($cmsBulunan && stripos(implode(' ', $cmsBulunan), 'WordPress') !== false) {
    $xmlrpcVar = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'xmlrpc.php';
    $xmlrpcVarMi = file_exists($xmlrpcVar);
    addCheck($configChecks, $riskPoints, $maxPoints, 'WordPress xmlrpc.php erişimi',
        $xmlrpcVarMi ? 'DIKKAT' : 'IYI',
        $xmlrpcVarMi ? 'xmlrpc.php mevcut - pingback özelliği kaba kuvvet amplifikasyonu ve DDoS için kötüye kullanılabilir. Kullanmiyorsaniz devre dışı bırakın (eklenti veya .htaccess ile).'
                     : 'xmlrpc.php bulunamadı/zaten kapalı.', 1);
}

/* ================= SPF / DMARC KAYDI (E-POSTA SAHTECILIGI) ================= */

if (!empty($_SERVER['HTTP_HOST']) && function_exists('dns_get_record')) {
    $dnsHost = preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
    $spfVarMi = false;
    $dmarcVarMi = false;
    $txtKayitlari = @dns_get_record($dnsHost, DNS_TXT);
    if ($txtKayitlari) {
        foreach ($txtKayitlari as $kayıt) {
            if (isset($kayıt['txt']) && stripos($kayıt['txt'], 'v=spf1') !== false) $spfVarMi = true;
        }
    }
    $dmarcKayitlari = @dns_get_record('_dmarc.' . $dnsHost, DNS_TXT);
    if ($dmarcKayitlari) {
        foreach ($dmarcKayitlari as $kayıt) {
            if (isset($kayıt['txt']) && stripos($kayıt['txt'], 'v=DMARC1') !== false) $dmarcVarMi = true;
        }
    }
    $eksikEposta = array_filter(['SPF' => $spfVarMi, 'DMARC' => $dmarcVarMi], fn($v) => !$v);
    addCheck($configChecks, $riskPoints, $maxPoints, 'SPF / DMARC kaydı (e-posta sahteciligi)',
        $eksikEposta ? 'DIKKAT' : 'IYI',
        $eksikEposta ? 'Eksik: ' . implode(', ', array_keys($eksikEposta)) . ' - bu alan adindan sahte e-posta (phishing) gönderilmesi daha kolay olabilir. DNS\'e SPF/DMARC TXT kaydı ekleyin.'
                     : 'SPF ve DMARC kayıtları mevcut.', 1);
}

/* ================= UPLOAD DİZİNİ PHP CALISTIRMA KORUMASI ================= */

$uploadDizinleri = ['uploads', 'upload', 'wp-content/uploads', 'files', 'images', 'media', 'assets/uploads'];
$korumasizUpload = [];
foreach ($uploadDizinleri as $ud) {
    $tam = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $ud);
    if (!is_dir($tam) || !is_writable($tam)) continue;
    $htYol = $tam . DIRECTORY_SEPARATOR . '.htaccess';
    $korumali = false;
    if (is_readable($htYol)) {
        $htIcerik = file_get_contents($htYol);
        if (preg_match('/php_flag\s+engine\s+off|SetHandler\s+none|RemoveHandler|deny\s+from\s+all|Require\s+all\s+denied/i', $htIcerik)) {
            $korumali = true;
        }
    }
    if (!$korumali) $korumasizUpload[] = $ud;
}
addCheck($configChecks, $riskPoints, $maxPoints, 'Upload dizini PHP çalıştırma koruması',
    $korumasizUpload ? 'RISKLI' : 'IYI',
    $korumasizUpload ? 'Yazılabilir ve korumasiz dizin(ler): ' . implode(', ', $korumasizUpload) . ' - bir dosya yükleme zafiyeti burada .php dosyası bırakıp çalıştırmaya (webshell) dönüşebilir. Bu dizinlere PHP calistirmayi engelleyen bir .htaccess (php_flag engine off / Require all denied) ekleyin.'
                        : 'Bilinen upload dizinlerinden yazılabilir olan bulunamadı ya da bulunanlar PHP çalıştırmaya karşı korunuyor.', 3);

/* ================= BAGIMLILIK DİZİNİ/DOSYASI İFŞASI (vendor, node_modules) ================= */

$bagimlilikIfsa = [];
if (file_exists(rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php')) {
    $bagimlilikIfsa[] = 'vendor/autoload.php (Composer bağımlılıkları)';
}
if (is_dir(rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'node_modules')) {
    $bagimlilikIfsa[] = 'node_modules/ (npm bağımlılıkları)';
}
addCheck($configChecks, $riskPoints, $maxPoints, 'Bağımlılık dizini ifşası (vendor/node_modules)',
    $bagimlilikIfsa ? 'RISKLI' : 'IYI',
    $bagimlilikIfsa ? 'Web kökünde erişilebilir: ' . implode(', ', $bagimlilikIfsa) . ' - kütüphane sürümleri ifşa olur ve bazi dosyalar (ornegin .php olmayan config/test dosyaları) doğrudan okunabilir olabilir. Bu dizinleri web kökünün disina taşıyın veya erişimi engelleyin.'
                       : 'vendor/ veya node_modules/ web kokunden doğrudan erişilebilir görünmüyor.', 2);

$bagimlilikDosyalari = array_filter([
    'composer.lock' => file_exists(rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'composer.lock'),
    'package-lock.json' => file_exists(rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'package-lock.json'),
    'yarn.lock' => file_exists(rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'yarn.lock'),
]);
if ($bagimlilikDosyalari) {
    addCheck($configChecks, $riskPoints, $maxPoints, 'Bilinen zafiyetli bağımlılık taramasi',
        'BILGI',
        'Bulunan kilit dosyaları: ' . implode(', ', array_keys($bagimlilikDosyalari)) . ' - bu panel paket iç CVE veritabanina bağlanmıyor, düzenli olarak sunucuda `composer audit` / `npm audit` çalıştırın.', 0);
}

/* ================= security.txt (IYI PRATIK) ================= */

$secTxtResp = selfIste('/.well-known/security.txt');
if ($secTxtResp) {
    $secTxtVarMi = $secTxtResp['code'] === 200 && stripos($secTxtResp['body'], 'Contact:') !== false;
    addCheck($configChecks, $riskPoints, $maxPoints, 'security.txt (RFC 9116)',
        $secTxtVarMi ? 'IYI' : 'BILGI',
        $secTxtVarMi ? '/.well-known/security.txt mevcut - güvenlik arastirmacilarinin size doğru kanaldan ulasmasini kolaylastirir.'
                     : 'security.txt bulunamadı (zorunlu değil ama iyi pratik). Eklemek isterseniz: securitytxt.org', 0);
}

/* ================= SESSION.USE_TRANS_SID (SESSION ID URL'DE) ================= */

$transSidAcik = (bool)ini_get('session.use_trans_sid');
addCheck($configChecks, $riskPoints, $maxPoints, 'session.use_trans_sid',
    $transSidAcik ? 'RISKLI' : 'IYI',
    $transSidAcik ? 'Açık - oturum ID\'si URL icine eklenebiliyor, bu da referrer sızıntısı/paylaşılan linkler üzerinden oturum calinmasina (session fixation) yol acabilir.'
                  : 'Kapalı - oturum ID sadece cerezle taşınıyor.', 2);

/* ================= EK PHP.INI VE OTURUM KONTROLLERİ ================= */

addCheck($configChecks, $riskPoints, $maxPoints, 'cgi.fix_pathinfo',
    ini_get('cgi.fix_pathinfo') === '1' ? 'DIKKAT' : 'IYI',
    ini_get('cgi.fix_pathinfo') === '1'
        ? 'Açık - PHP CGI/FPM + Nginx yanlış konfigure edilmisse dosya adi sonuna PATH_INFO eklenerek baska dosyalarin PHP olarak calistirilmasina (CVE-2012-1823 sınıfı) yol acabilir.'
        : 'Kapalı/varsayılan değil.', 2);

addCheck($configChecks, $riskPoints, $maxPoints, 'enable_dl',
    ini_get('enable_dl') ? 'RISKLI' : 'IYI',
    ini_get('enable_dl') ? 'Açık - dl() ile çalışırken rastgele paylaşımlı kütüphane (.so/.dll) yüklenebilir, bu ciddi bir RCE vektoru olabilir.'
                         : 'Kapalı - dl() ile dinamik eklenti yüklenemez.', 2);

addCheck($configChecks, $riskPoints, $maxPoints, 'session.use_strict_mode',
    ini_get('session.use_strict_mode') ? 'IYI' : 'DIKKAT',
    ini_get('session.use_strict_mode') ? 'Açık - PHP disaridan gelen/uninitialized session ID\'lerini kabul etmiyor.'
                                        : 'Kapalı - saldirgan bilinen bir session ID\'yi kurbana enjekte edip oturum sabitleme (fixation) yapabilir.', 2);

addCheck($configChecks, $riskPoints, $maxPoints, 'session.use_only_cookies',
    ini_get('session.use_only_cookies') ? 'IYI' : 'RISKLI',
    ini_get('session.use_only_cookies') ? 'Açık - oturum ID sadece cerezle tasinabiliyor.'
                                         : 'Kapalı - oturum ID URL/GET parametresiyle de taşınabilir, session hijacking riskini ciddi artırır.', 2);

$assertAktif = ini_get('assert.active');
addCheck($configChecks, $riskPoints, $maxPoints, 'assert.active',
    $assertAktif ? 'DIKKAT' : 'IYI',
    $assertAktif ? 'Açık - eski PHP surumlerinde assert() string argumanla eval benzeri davranabilir, kod çalıştırma riski taşır.'
                 : 'Kapalı/varsayılan değil.', 1);

if (PHP_VERSION_ID >= 80000) {
    addCheck($configChecks, $riskPoints, $maxPoints, 'zend.exception_ignore_args',
        ini_get('zend.exception_ignore_args') ? 'IYI' : 'DIKKAT',
        ini_get('zend.exception_ignore_args') ? 'Açık - hata/log ciktilarinda fonksiyon argumanlari (parolalar dahil olabilir) gizleniyor.'
                                               : 'Kapalı - stack trace loglarinda fonksiyonlara geçirilen hassas değerler (parola, token) gorunebilir.', 1);
}

addCheck($configChecks, $riskPoints, $maxPoints, 'log_errors',
    ini_get('log_errors') ? 'IYI' : 'DIKKAT',
    ini_get('log_errors') ? 'Açık - hatalar display_errors kapalı olsa bile kayıt altina alınıyor.'
                          : 'Kapalı - display_errors da kapalıysa hatalar hiçbir yerde görünmüyor, saldırı denemeleri fark edilmeyebilir.', 1);

addCheck($configChecks, $riskPoints, $maxPoints, 'html_errors',
    ini_get('html_errors') ? 'BILGI' : 'IYI',
    ini_get('html_errors') ? 'Açık - hata çıktıları HTML formatinda (docs.php.net linkleri dahil), display_errors ile birlikte açık kalirsa bilgi ifsasini artırır.'
                           : 'Kapalı - hata çıktıları düz metin.', 0);

$defaultCharset = strtolower((string)ini_get('default_charset'));
addCheck($configChecks, $riskPoints, $maxPoints, 'default_charset',
    strpos($defaultCharset, 'utf-8') !== false ? 'IYI' : 'DIKKAT',
    strpos($defaultCharset, 'utf-8') !== false ? "Tanımlı: $defaultCharset"
        : "Tanımlı: $defaultCharset - UTF-8 dışı/eksik charset bazi tarayicilarda charset tabanli XSS filtre atlatma tekniklerine zemin hazirlayabilir.", 1);

$maxInputVars = (int)ini_get('max_input_vars');
addCheck($configChecks, $riskPoints, $maxPoints, 'max_input_vars',
    $maxInputVars > 5000 ? 'DIKKAT' : 'IYI',
    $maxInputVars > 5000 ? "Çok yüksek ($maxInputVars) - hash-collision tabanli DoS saldirilarina yuzey genişletebilir."
                          : "Tanımlı: $maxInputVars", 1);

$gcMaxlifetime = (int)ini_get('session.gc_maxlifetime');
addCheck($configChecks, $riskPoints, $maxPoints, 'session.gc_maxlifetime',
    $gcMaxlifetime > 86400 * 7 ? 'DIKKAT' : 'IYI',
    $gcMaxlifetime > 86400 * 7 ? "Çok uzun ($gcMaxlifetime sn) - oturumlar haftalarca geçerli kalabilir, calinan bir çerez uzun süre kullanılabilir olur."
                               : "Tanımlı: $gcMaxlifetime sn", 1);

if (!$isWindows) {
    $uploadTmp = ini_get('upload_tmp_dir') ?: sys_get_temp_dir();
    if (is_dir($uploadTmp)) {
        $uploadTmpAcik = (bool)(@fileperms($uploadTmp) & 0002);
        addCheck($configChecks, $riskPoints, $maxPoints, 'upload_tmp_dir izinleri',
            $uploadTmpAcik ? 'DIKKAT' : 'IYI',
            $uploadTmpAcik ? "Geçici yükleme dizini ($uploadTmp) herkes tarafından yazılabilir - race condition ile yükleme sırasında dosyaya erişim riski taşır."
                           : 'Geçici yükleme dizini herkes tarafından yazılabilir görünmüyor.', 1);
    }
}

$postMax = ini_get('post_max_size');
$uploadMax = ini_get('upload_max_filesize');
addCheck($configChecks, $riskPoints, $maxPoints, 'post_max_size / upload_max_filesize tutarlılığı',
    'BILGI',
    "post_max_size=$postMax, upload_max_filesize=$uploadMax - post_max_size, upload_max_filesize'dan kucukse büyük dosya yuklemeleri sessizce basarisiz olabilir (güvenlik değil, operasyonel not).", 0);

$tzTanimli = (bool)ini_get('date.timezone');
addCheck($configChecks, $riskPoints, $maxPoints, 'date.timezone',
    $tzTanimli ? 'IYI' : 'BILGI',
    $tzTanimli ? 'Tanımlı: ' . ini_get('date.timezone') : 'Tanımlı değil - güvenlik riski değil ama her cagrida uyari/performans maliyeti dogurabilir.', 0);

addCheck($configChecks, $riskPoints, $maxPoints, 'register_argc_argv',
    ini_get('register_argc_argv') ? 'BILGI' : 'IYI',
    ini_get('register_argc_argv') ? 'Açık - web bağlamında genelde gereksiz, küçük bir bellek/performans maliyeti dışında önemli risk taşımaz.'
                                   : 'Kapalı - web bağlamında önerilen durum.', 0);

addCheck($configChecks, $riskPoints, $maxPoints, 'file_uploads',
    'BILGI',
    ini_get('file_uploads') ? 'Açık - uygulamanız dosya yüklemesi kullanmıyorsa kapatmak saldırı yüzeyini azaltır.' : 'Kapalı - dosya yükleme tamamen devre dışı.', 0);

addCheck($configChecks, $riskPoints, $maxPoints, 'short_open_tag',
    ini_get('short_open_tag') ? 'BILGI' : 'IYI',
    ini_get('short_open_tag') ? 'Açık - "<?" kisa etiketi çalışıyor, XML islemleriyle nadiren çakışabilir; güvenlik acisindan kritik değil.'
                               : 'Kapalı - sadece "<?php" etiketi çalışır.', 0);

$sidBits = (int)ini_get('session.sid_bits_per_character');
$sidLength = (int)ini_get('session.sid_length');
$sidEntropy = $sidBits * $sidLength;
addCheck($configChecks, $riskPoints, $maxPoints, 'Session ID entropisi (sid_length x sid_bits_per_character)',
    $sidEntropy > 0 && $sidEntropy < 128 ? 'DIKKAT' : 'IYI',
    $sidEntropy > 0
        ? "Tahmini entropi: {$sidEntropy} bit (sid_length=$sidLength, sid_bits_per_character=$sidBits)" . ($sidEntropy < 128 ? ' - 128 bitin altinda, tahmin edilebilirligi artırır.' : '.')
        : 'Degerler okunamadı.', 1);

/* ================= EK PHP GÜVENLİK AYARLARI (2. TUR) ================= */

$cookieLifetime = (int)ini_get('session.cookie_lifetime');
addCheck($configChecks, $riskPoints, $maxPoints, 'session.cookie_lifetime',
    $cookieLifetime > 86400 * 30 ? 'DIKKAT' : 'IYI',
    $cookieLifetime === 0
        ? 'Tanımlı: 0 (tarayıcı kapaninca silinir) - iyi bir varsayılan.'
        : ($cookieLifetime > 86400 * 30
            ? "Tanımlı: {$cookieLifetime} sn - 30 gunden uzun kalici oturum çerezleri, calinan bir cihazda uzun süreli ele gecirme riski taşır."
            : "Tanımlı: {$cookieLifetime} sn"), 1);

$mysqliLocalInfile = ini_get('mysqli.allow_local_infile');
addCheck($configChecks, $riskPoints, $maxPoints, 'mysqli.allow_local_infile',
    $mysqliLocalInfile ? 'DIKKAT' : 'IYI',
    $mysqliLocalInfile ? 'Açık - kötü niyetli/ele geçirilmiş bir MySQL sunucusu, uygulamanızdan yerel dosya okumasi (LOAD DATA LOCAL INFILE) isteyebilir. Gerekmiyorsa kapatin.'
                        : 'Kapalı/varsayılan değil.', 1);

$pdoMysqlLocalInfile = ini_get('pdo_mysql.allow_local_infile');
addCheck($configChecks, $riskPoints, $maxPoints, 'pdo_mysql.allow_local_infile',
    $pdoMysqlLocalInfile ? 'DIKKAT' : 'IYI',
    $pdoMysqlLocalInfile ? 'Açık - PDO üzerinden de LOAD DATA LOCAL INFINE ile yerel dosya okuma riski taşır. Gerekmiyorsa kapatin.'
                         : 'Kapalı/varsayılan değil.', 1);

$pharReadonly = ini_get('phar.readonly');
addCheck($configChecks, $riskPoints, $maxPoints, 'phar.readonly',
    $pharReadonly ? 'IYI' : 'DIKKAT',
    $pharReadonly ? 'Açık (varsayılan) - yeni .phar dosyaları olusturulamiyor, phar deserializasyon zincirlerine karşı ek bir katman.'
                  : 'Kapalı - eğer uygulamanız .phar oluşturmuyorsa açık bırakmanın bir faydası yok, kapatmayi değerlendirin.', 1);

if ($dirListResp) {
    preg_match('/^Server:\s*([^\r\n]+)/im', $dirListResp['headers'], $srvm);
    $serverBasligi = trim($srvm[1] ?? '');
    $serverAyrintili = $serverBasligi && preg_match('/\d/', $serverBasligi);
    addCheck($configChecks, $riskPoints, $maxPoints, 'Server başlığı ayrıntısı',
        $serverAyrintili ? 'DIKKAT' : 'IYI',
        $serverAyrintili ? "Gönderilen: \"$serverBasligi\" - sürüm numarası içeriyor, saldırgana hangi CVE'leri deneyeceğini söyler. ServerTokens Prod (Apache) / server_tokens off (Nginx) ile sadeleştirin."
                          : ($serverBasligi ? "Gönderilen: \"$serverBasligi\" - sürüm bilgisi içermiyor, iyi." : 'Server başlığı tespit edilemedi.'), 1);
}

/* ================= PHP EKLENTI ENVANTERI (riskli/dikkat gerektiren uzantilar) ================= */

$ilginc_eklentiler = [
    'sockets'  => 'Ham TCP/UDP soket açabilir; SSRF sonrası ağ taramasını kolaylaştırabilir.',
    'ftp'      => 'FTP istemcisi; kimlik bilgisi düz metin taşıyabilen eski bir protokol.',
    'ldap'     => 'LDAP istemcisi; enjeksiyon (LDAP injection) yüzeyi oluşturabilir.',
    'imap'     => 'IMAP/POP3 istemcisi; geçmişte ciddi RCE CVE\'leri (CVE-2018-19518 vb.) barındırdı.',
    'soap'     => 'SOAP istemcisi; XML tabanlı deserializasyon/SSRF senaryolarına zemin olabilir.',
    'exif'     => 'Görsel meta veri ayrıştırıcı; geçmişte parser tabanlı bellek bozulması CVE\'leri oldu.',
    'phar'     => 'Phar arşiv işleyici; "phar://" akış sarmalayıcısı deserializasyon zincirlerinde kullanılabilir.',
];
foreach ($ilginc_eklentiler as $extAd => $aciklama) {
    $extYukluMu = extension_loaded($extAd);
    addCheck($configChecks, $riskPoints, $maxPoints, "PHP eklentisi: $extAd",
        $extYukluMu ? 'DIKKAT' : 'IYI',
        $extYukluMu ? "Yüklü. $aciklama Uygulamanız kullanmıyorsa php.ini'den devre dışı bırakmayı değerlendirin (saldırı yüzeyini azaltır)."
                    : 'Yüklü değil.', 0);
}

addCheck($configChecks, $riskPoints, $maxPoints, 'session.upload_progress.enabled',
    ini_get('session.upload_progress.enabled') ? 'DIKKAT' : 'IYI',
    ini_get('session.upload_progress.enabled')
        ? 'Açık - eski PHP sürümlerinde (CVE-2015-4025/4026 sınıfı) yükleme ilerleme oturum verisiyle ilgili zafiyetlere karışmıştı; gerekmiyorsa kapatın.'
        : 'Kapalı/varsayılan değil.', 1);

$mysqliVarMi = extension_loaded('mysqli');
$pdoMysqlVarMi = extension_loaded('pdo_mysql');
addCheck($configChecks, $riskPoints, $maxPoints, 'MySQL PHP sürücüleri (mysqli / pdo_mysql)',
    'BILGI',
    'mysqli: ' . ($mysqliVarMi ? 'yüklü' : 'yüklü değil') . ', pdo_mysql: ' . ($pdoMysqlVarMi ? 'yüklü' : 'yüklü değil')
        . '. Modern uygulamalar hazır ifadeler (prepared statements) destekleyen bu sürücüleri kullanmalı, eski "mysql_*" fonksiyonlarından kaçınılmalı.', 0);

/* ================= APACHE MODÜLLERİ VE VARSAYILAN DOSYALAR ================= */

if (function_exists('apache_get_modules')) {
    $apacheModulleri = apache_get_modules();
    $riskliModuller = ['mod_autoindex' => 'dizin listeleme', 'mod_userdir' => 'kullanıcı ana dizini ifşası', 'mod_negotiation' => 'MultiViews ile dosya adı tahmini/kaynak kod ifşası'];
    $bulunanRiskli = array_intersect_key($riskliModuller, array_flip($apacheModulleri));
    addCheck($configChecks, $riskPoints, $maxPoints, 'Apache aktif modülleri',
        $bulunanRiskli ? 'DIKKAT' : 'IYI',
        $bulunanRiskli
            ? 'Dikkat çeken modüller aktif: ' . implode(', ', array_map(fn($m, $a) => "$m ($a)", array_keys($bulunanRiskli), $bulunanRiskli)) . '. Gerekmiyorsa a2dismod ile kapatın.'
            : 'Bilinen riskli/gereksiz modüllerden (mod_autoindex, mod_userdir, mod_negotiation) hiçbiri listede görünmüyor.', 1);

    $modSecurityVarMi = in_array('mod_security2', $apacheModulleri, true) || in_array('mod_security', $apacheModulleri, true);
    addCheck($configChecks, $riskPoints, $maxPoints, 'mod_security (WAF) varlığı',
        $modSecurityVarMi ? 'IYI' : 'BILGI',
        $modSecurityVarMi ? 'mod_security yüklü görünüyor - uygulama seviyesinde ek bir savunma katmanı sağlar.'
                          : 'mod_security tespit edilemedi. Bir WAF (mod_security, Cloudflare vb.) eklemeyi değerlendirin.', 0);
} else {
    addCheck($configChecks, $riskPoints, $maxPoints, 'Apache aktif modülleri',
        'BILGI',
        'apache_get_modules() kullanılamıyor (PHP Apache modülü olarak değil, FPM/CGI olarak çalışıyor olabilir) - modül listesi bu şekilde tespit edilemedi.', 0);
}

$apacheVarsayilanYollar = ['manual', 'icons', 'cgi-bin', 'doc'];
foreach ($apacheVarsayilanYollar as $avy) {
    $avyTam = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . $avy;
    $avyVarMi = is_dir($avyTam);
    addCheck($configChecks, $riskPoints, $maxPoints, "Apache/dağıtım varsayılan dizini: /$avy",
        $avyVarMi ? 'DIKKAT' : 'IYI',
        $avyVarMi ? "/$avy dizini web kökünde bulundu - genelde varsayılan kurulum artığıdır, gerekmiyorsa kaldırın."
                  : "/$avy web kökünde bulunamadı.", 0);
}

/* ================= NGINX DURUM SAYFASI IFŞASI (stub_status) ================= */

$nginxStatusResp = selfIste('/nginx_status') ?: selfIste('/stub_status');
if ($nginxStatusResp) {
    $nginxStatusAcik = $nginxStatusResp['code'] === 200 && stripos($nginxStatusResp['body'], 'Active connections') !== false;
    addCheck($configChecks, $riskPoints, $maxPoints, 'Nginx stub_status ifşası',
        $nginxStatusAcik ? 'RISKLI' : 'IYI',
        $nginxStatusAcik ? '/nginx_status veya /stub_status herkese açık yayınlanıyor; aktif bağlantı sayıları ifşa oluyor. nginx.conf\'ta bu location\'ı "allow 127.0.0.1; deny all;" ile kısıtlayın.'
                         : 'Nginx durum sayfası dışarıdan erişilebilir/yayında görünmüyor.', 1);
}

/* ================= PHP-FPM DURUM SAYFASI IFŞASI ================= */

if (php_sapi_name() === 'fpm-fcgi') {
    $fpmStatusResp = selfIste('/status') ?: selfIste('/fpm-status');
    if ($fpmStatusResp) {
        $fpmStatusAcik = $fpmStatusResp['code'] === 200 && (stripos($fpmStatusResp['body'], 'pool') !== false || stripos($fpmStatusResp['body'], 'process manager') !== false);
        addCheck($configChecks, $riskPoints, $maxPoints, 'PHP-FPM durum sayfası ifşası',
            $fpmStatusAcik ? 'RISKLI' : 'IYI',
            $fpmStatusAcik ? '/status PHP-FPM havuz bilgisini herkese açık gösteriyor - istek sayıları, dosya yolları ifşa olabilir. Nginx/Apache tarafında bu location\'ı kısıtlayın.'
                           : 'PHP-FPM durum sayfası dışarıdan erişilebilir görünmüyor.', 1);
    }
}

/* ================= MYSQL/MARIADB SOKET VE YAPILANDIRMA DOSYASI IZINLERI ================= */

if (!$isWindows) {
    $mysqlSoketYollari = ['/var/run/mysqld/mysqld.sock', '/tmp/mysql.sock', '/var/lib/mysql/mysql.sock'];
    $mysqlSoketBulunan = null;
    foreach ($mysqlSoketYollari as $msy) {
        if (file_exists($msy)) { $mysqlSoketBulunan = $msy; break; }
    }
    if ($mysqlSoketBulunan) {
        $soketAcik = (bool)(@fileperms($mysqlSoketBulunan) & 0006);
        addCheck($configChecks, $riskPoints, $maxPoints, 'MySQL/MariaDB soket dosyası izinleri',
            $soketAcik ? 'DIKKAT' : 'IYI',
            $soketAcik ? "$mysqlSoketBulunan diğer kullanıcılar tarafından okunabilir/yazılabilir - paylaşımlı sunucuda risk oluşturabilir."
                       : "$mysqlSoketBulunan bulundu, izinleri makul görünüyor.", 1);
    }

    $myCnfYollari = ['/etc/mysql/my.cnf', '/etc/my.cnf'];
    foreach ($myCnfYollari as $mcy) {
        if (is_readable($mcy)) {
            $mcyIcerik = file_get_contents($mcy, false, null, 0, 4000);
            $localInfileAcik = (bool)preg_match('/local[_-]infile\s*=\s*1/i', $mcyIcerik);
            addCheck($configChecks, $riskPoints, $maxPoints, 'MySQL/MariaDB my.cnf: local_infile',
                $localInfileAcik ? 'DIKKAT' : 'IYI',
                $localInfileAcik ? "$mcy içinde local_infile=1 tanımlı - LOAD DATA LOCAL INFILE ile dosya okuma riski sunucu tarafında da açık."
                                 : "$mcy okundu, local_infile açık tanımlanmamış.", 1);
            break;
        }
    }
}

$webRootMyCnf = rtrim($docRoot, '/\\') . DIRECTORY_SEPARATOR . 'my.cnf';
addCheck($configChecks, $riskPoints, $maxPoints, 'my.cnf web kökünde ifşası',
    file_exists($webRootMyCnf) ? 'RISKLI' : 'IYI',
    file_exists($webRootMyCnf) ? 'my.cnf belge kökünde bulundu - veritabanı bağlantı bilgilerini içerebilir, hemen taşıyın/silin.'
                                : 'my.cnf belge kökünde bulunamadı.', 2);

/* ================= GENEL RISK SKORU ================= */

$scorePct = $maxPoints > 0 ? round((1 - ($riskPoints / $maxPoints)) * 100) : 100;
$scoreLabel = $scorePct >= 85 ? 'İYİ' : ($scorePct >= 60 ? 'ORTA' : 'KÖTÜ');
$scoreColor = $scorePct >= 85 ? '#a3e635' : ($scorePct >= 60 ? '#facc15' : '#ff6b6b');

/* ================= ONEM SIRASINA GORE SIRALAMA ================= */

function siralamaOnceligi($durum) {
    $siralar = ['RISKLI' => 0, 'BILINMIYOR' => 1, 'DIKKAT' => 1, 'BELIRSIZ' => 1, 'IYI' => 2, 'BILGI' => 3];
    return $siralar[$durum] ?? 2;
}

// İç mantık (karşılaştırma/CSS sınıfı) ASCII sabitler üzerinden çalışır (güvenli/tutarlı olması için),
// bu fonksiyon sadece EKRANDA GÖSTERİLEN metni düzgün Türkçe karakterlerle üretir.
function durumGoster($durum) {
    $harita = [
        'IYI' => 'İYİ', 'RISKLI' => 'RİSKLİ', 'DIKKAT' => 'DİKKAT',
        'BILGI' => 'BİLGİ', 'BILINMIYOR' => 'BİLİNMİYOR', 'BELIRSIZ' => 'BELİRSİZ',
    ];
    return $harita[$durum] ?? $durum;
}
usort($checks, fn($a, $b) => siralamaOnceligi($a['durum']) <=> siralamaOnceligi($b['durum']));
usort($configChecks, fn($a, $b) => siralamaOnceligi($a['durum']) <=> siralamaOnceligi($b['durum']));

$riskliSayisi = count(array_filter(array_merge($checks, $configChecks), fn($c) => $c['durum'] === 'RISKLI'));
$dikkatSayisi = count(array_filter(array_merge($checks, $configChecks), fn($c) => in_array($c['durum'], ['DIKKAT', 'BILINMIYOR', 'BELIRSIZ'], true)));

/* ================= EK SİSTEM BİLGİSİ (Ham Çıktı bölümü için) ================= */

$phpSapi = php_sapi_name();
$yukluEklentiler = get_loaded_extensions();
$osUname = php_uname();
$hostname = gethostname() ?: 'bilinmiyor';
$serverIp = $_SERVER['SERVER_ADDR'] ?? null;
$memLimit = ini_get('memory_limit');
$diskTotal = @disk_total_space($docRoot);
$diskFree = @disk_free_space($docRoot);
$timezone = date_default_timezone_get();
$xdebugVer = extension_loaded('xdebug') ? phpversion('xdebug') : null;
$composerVer = run('composer --version');
$nodeVer = run('node --version');
$npmVer = run('npm --version');
$gitVer = run('git --version');
preg_match('/\d+\.\d+(\.\d+)?/', (string)$composerVer, $cvm);
preg_match('/v?(\d+\.\d+\.\d+)/', (string)$nodeVer, $nvm);
preg_match('/(\d+\.\d+\.\d+)/', (string)$npmVer, $npvm);
preg_match('/(\d+\.\d+(\.\d+)?)/', (string)$gitVer, $gvm);

function bayt($b) {
    if (!$b || $b <= 0) return 'bilinmiyor';
    $birimler = ['B','KB','MB','GB','TB'];
    $i = (int)floor(log($b, 1024));
    $i = max(0, min($i, count($birimler) - 1));
    return round($b / (1024 ** $i), 1) . ' ' . $birimler[$i];
}

if ($isWindows) {
    $cpuCores = (int)($_SERVER['NUMBER_OF_PROCESSORS'] ?? 0) ?: null;
    $uptimeStr = run('net statistics workstation | findstr /B /C:"Statistics since"') ?: null;
    $ramStr = null;
    $wmicMem = run('wmic OS get FreePhysicalMemory,TotalVisibleMemorySize /Value');
    if ($wmicMem && preg_match('/FreePhysicalMemory=(\d+)/', $wmicMem, $fm) && preg_match('/TotalVisibleMemorySize=(\d+)/', $wmicMem, $tm)) {
        $ramStr = bayt($fm[1] * 1024) . ' boş / ' . bayt($tm[1] * 1024) . ' toplam';
    }
    $yukStr = null;
    $suanCalisanSurec = run('tasklist /NH | find /C /V ""');
} else {
    $cpuCores = (int)(run('nproc') ?: 0) ?: null;
    $uptimeStr = run('uptime -p') ?: null;
    $ramStr = null;
    $memInfo = @file_get_contents('/proc/meminfo');
    if ($memInfo && preg_match('/MemTotal:\s+(\d+) kB/', $memInfo, $tm) && preg_match('/MemAvailable:\s+(\d+) kB/', $memInfo, $fm)) {
        $ramStr = bayt($fm[1] * 1024) . ' boş / ' . bayt($tm[1] * 1024) . ' toplam';
    }
    $yukStr = null;
    if (function_exists('sys_getloadavg')) {
        $yuk = sys_getloadavg();
        if ($yuk) $yukStr = round($yuk[0], 2) . ' / ' . round($yuk[1], 2) . ' / ' . round($yuk[2], 2) . ' (1dk/5dk/15dk)';
    }
    $suanCalisanSurec = run('ps -e --no-headers | wc -l');
}

$phpIniYolu = php_ini_loaded_file() ?: null;
$serverYazilimi = $_SERVER['SERVER_SOFTWARE'] ?? null;
$serverPort = $_SERVER['SERVER_PORT'] ?? null;
$httpsAktifMi = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
$phpCalistiranKullanici = function_exists('posix_geteuid') && function_exists('posix_getpwuid')
    ? (posix_getpwuid(posix_geteuid())['name'] ?? null)
    : (get_current_user() ?: null);
$diskYuzde = ($diskTotal && $diskTotal > 0) ? round((($diskTotal - $diskFree) / $diskTotal) * 100, 1) : null;

?>
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apache / PHP / MySQL Güvenlik Denetimi</title>
<style>
  * { box-sizing: border-box; }
  html { scroll-behavior: smooth; }
  body {
    margin: 0;
    font-family: Consolas, 'Courier New', monospace;
    background: #0b0f1e;
    color: #d8dee9;
    padding: 20px;
    max-width: 1100px;
    margin-inline: auto;
  }
  .topbar { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:12px; margin-bottom:16px; }
  h1 { color: #ff4d4d; font-size: clamp(17px, 3vw, 22px); margin: 0 0 4px 0; }
  .sub { color: #8891a8; margin-bottom: 20px; font-size: 12.5px; word-break: break-word; }
  .toolbar {
    display:flex; gap:2px; align-items:center; flex-wrap:nowrap;
    background:#0d1326; border:1px solid #1c2540; border-radius:10px;
    padding:5px; max-width:100%; overflow-x:auto;
  }
  .toolbar::-webkit-scrollbar { height:4px; }
  .toolbar::-webkit-scrollbar-thumb { background:#24304f; border-radius:4px; }
  .toolbar label {
    font-size: 12px; color:#8891a8; display:flex; align-items:center; gap:6px; cursor:pointer;
    padding:6px 10px; border-radius:6px; white-space:nowrap; flex-shrink:0;
    transition: background .15s, color .15s;
  }
  .toolbar label:hover { background:#1a2340; color:#d8dee9; }
  .toolbar input[type="checkbox"] { accent-color:#7dd3fc; width:14px; height:14px; flex-shrink:0; }
  .toolbar .sep { width:1px; height:18px; background:#24304f; flex-shrink:0; margin:0 3px; }
  .logout {
    color:#a9b4c9; font-size:12px; text-decoration:none;
    padding:6px 10px; border-radius:6px; white-space:nowrap; flex-shrink:0;
    transition: background .15s, color .15s;
  }
  .logout:hover { background:#1a2340; color:#fff; }
  .logout.danger { color:#ff9d9d; }
  .logout.danger:hover { background:rgba(255,107,107,0.14); color:#ff6b6b; }
  .warn {
    background: #2a1c1c;
    border: 1px solid #7a3b3b;
    color: #ffb3b3;
    padding: 10px 14px;
    border-radius: 8px;
    font-size: 13px;
    margin-bottom: 20px;
    line-height: 1.5;
  }
  .navpills {
    display:flex; gap:8px; flex-wrap:wrap; margin-bottom:20px;
    position: sticky; top: 0; z-index: 10;
    background: rgba(11,15,30,0.92); backdrop-filter: blur(4px);
    padding: 10px 0; border-bottom: 1px solid #24304f;
  }
  .navpills a {
    font-size:12px; color:#7dd3fc; text-decoration:none; border:1px solid #24304f;
    padding:5px 10px; border-radius:20px; white-space:nowrap;
  }
  .navpills a:hover { border-color:#7dd3fc; }
  .navpills a[data-active="1"] { background:#7dd3fc; color:#0b0f1e; border-color:#7dd3fc; }
  .kritik-box {
    background: rgba(255,107,107,0.06);
    border: 1px solid rgba(255,107,107,0.35);
    border-radius: 12px;
    padding: 14px 18px;
    margin-bottom: 20px;
  }
  .kritik-box h3 { margin:0 0 8px 0; font-size:13px; color:#ff8a8a; text-transform:uppercase; letter-spacing:0.5px; }
  .kritik-box ul { margin:0; padding-left:18px; }
  .kritik-box li { font-size:13px; margin-bottom:5px; line-height:1.5; }
  .kritik-box b { color:#ff9d9d; }
  .score-box {
    display:flex; align-items:center; gap:24px; flex-wrap:wrap;
    background:linear-gradient(180deg, #141c33, #131a2e);
    border:1px solid #24304f; border-left: 4px solid <?= $scoreColor ?>;
    border-radius:12px; padding:20px 24px; margin-bottom:20px;
    box-shadow: 0 4px 18px rgba(0,0,0,0.25);
  }
  .score-ring { flex-shrink:0; }
  .score-ring .ring-bg { fill:none; stroke:#1c2540; stroke-width:9; }
  .score-ring .ring-fg {
    fill:none; stroke:<?= $scoreColor ?>; stroke-width:9; stroke-linecap:round;
    transform-origin: 50% 50%; transform: rotate(-90deg);
    transition: stroke-dashoffset 0.8s ease;
  }
  .score-ring text { fill:<?= $scoreColor ?>; font-size:26px; font-weight:bold; font-family:Consolas,monospace; }
  .score-label { font-size:13px; color:#8891a8; line-height:1.7; }
  .score-label b { color: <?= $scoreColor ?>; }
  .stat-chips { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
  .stat-chip {
    display:flex; align-items:center; gap:6px; font-size:12px; font-weight:bold;
    padding:4px 10px; border-radius:20px;
  }
  .stat-chip .dot { width:7px; height:7px; border-radius:50%; }
  .stat-chip.risk { background:rgba(255,107,107,0.12); color:#ff6b6b; }
  .stat-chip.risk .dot { background:#ff6b6b; }
  .stat-chip.uyari { background:rgba(250,204,21,0.12); color:#facc15; }
  .stat-chip.uyari .dot { background:#facc15; }
  .stat-chip.ok { background:rgba(163,230,53,0.12); color:#a3e635; }
  .stat-chip.ok .dot { background:#a3e635; }
  .stat-chip:hover { filter:brightness(1.15); }
  .filterbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:12px; }
  .filterbar input[type="search"] {
    flex:1; min-width:160px; background:#0b0f1e; border:1px solid #24304f; color:#d8dee9;
    padding:7px 12px; border-radius:20px; font-family:inherit; font-size:12.5px;
  }
  .filterbar input[type="search"]:focus { outline:none; border-color:#7dd3fc; }
  .filter-pill {
    font-size:11.5px; font-weight:bold; padding:5px 12px; border-radius:20px; cursor:pointer;
    border:1px solid #24304f; background:transparent; color:#8891a8; white-space:nowrap;
    transition: all .15s;
  }
  .filter-pill[data-active="1"] { background:#1a2340; color:#d8dee9; border-color:#7dd3fc; }
  #kategoriFiltre {
    font-size:11.5px; font-weight:bold; padding:5px 10px; border-radius:20px;
    border:1px solid #24304f; background:#0b0f1e; color:#8891a8; font-family:inherit; cursor:pointer;
  }
  #kategoriFiltre:focus { outline:none; border-color:#7dd3fc; }
  .kat-etiket {
    font-size:10.5px; color:#8891a8; background:#1a2340; padding:2px 8px; border-radius:10px; white-space:nowrap;
  }
  .satir-sayaci { font-size:11.5px; color:#5b6779; margin-bottom:8px; }
  .tablo-altbaslik {
    font-size:10.5px; color:#7dd3fc; text-transform:uppercase; letter-spacing:0.6px;
    background:#0d1326 !important; font-weight:bold; padding-top:10px !important; padding-bottom:6px !important;
  }
  .tablo-altbaslik:first-child { padding-top:9px !important; }
  .log-tablosu { min-width:auto; font-size:11.5px; }
  .log-tablosu th { font-size:10px; padding:6px 8px; }
  .log-tablosu td { padding:5px 8px; }
  .log-tablosu .pill { font-size:9.5px; padding:1px 7px; }
  .log-tablosu .pill::before { width:5px; height:5px; }
  .log-ua {
    max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    color:#5b6779; font-size:11px;
  }
  .sayfalama { display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-top:12px; }
  .sayfa-bilgi { font-size:11px; color:#5b6779; margin-right:6px; }
  .sayfa-btn {
    font-size:11.5px; font-weight:bold; min-width:28px; padding:5px 8px; border-radius:6px;
    border:1px solid #24304f; background:#0b0f1e; color:#8891a8; cursor:pointer; font-family:inherit;
    transition: all .15s;
  }
  .sayfa-btn:hover { border-color:#7dd3fc; color:#d8dee9; }
  .sayfa-btn.aktif { background:#7dd3fc; color:#0b0f1e; border-color:#7dd3fc; }
  .sayfa-btn:disabled { opacity:0.35; cursor:not-allowed; }
  .sayfa-ellipsis { color:#5b6779; font-size:11.5px; padding:0 2px; }
  .yükleniyor-durum { opacity:0.45; transition: opacity .15s; pointer-events:none; }
  .log-tablosu tbody tr { animation: satirGir .25s ease; }
  @keyframes satirGir { from { opacity:0; } to { opacity:1; } }
  .olay-rozet {
    display:inline-block; font-size:10px; font-weight:bold; text-transform:uppercase; letter-spacing:0.3px;
    padding:2px 8px; border-radius:10px; white-space:nowrap;
  }
  .olay-giris { background:rgba(125,211,252,0.12); color:#7dd3fc; }
  .olay-kilit { background:rgba(255,107,107,0.14); color:#ff6b6b; }
  .olay-notr  { background:rgba(139,148,171,0.15); color:#a9b4c9; }
  .table-wrap { overflow-x: auto; margin-bottom: 4px; }
  table {
    width: 100%;
    min-width: 480px;
    border-collapse: collapse;
    font-size: 13px;
  }
  th, td {
    text-align: left;
    padding: 9px 10px;
    border-bottom: 1px solid #24304f;
    vertical-align: top;
  }
  td:first-child { white-space: nowrap; }
  tbody tr { transition: background .12s; }
  tbody tr:hover { background: rgba(125,211,252,0.05); }
  th { color: #7dd3fc; text-transform: uppercase; font-size: 11.5px; letter-spacing: 0.5px; }
  .pill {
    display:inline-flex; align-items:center; gap:6px; padding:2px 10px; border-radius:20px; font-size:11px;
    font-weight:bold; letter-spacing:0.3px; white-space:nowrap;
  }
  .pill::before { content:''; width:6px; height:6px; border-radius:50%; background:currentColor; flex-shrink:0; }
  .durum-iyi       { background:rgba(163,230,53,0.12); color:#a3e635; }
  .durum-riskli    { background:rgba(255,107,107,0.14); color:#ff6b6b; }
  .durum-bilinmiyor, .durum-belirsiz, .durum-dikkat { background:rgba(250,204,21,0.12); color:#facc15; }
  .durum-bilgi     { background:rgba(125,211,252,0.12); color:#7dd3fc; }
  .box {
    background: #131a2e;
    border: 1px solid #24304f;
    border-radius: 12px;
    padding: 16px 18px;
    margin-bottom: 20px;
    scroll-margin-top: 60px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.15);
    animation: belirme .35s ease;
  }
  @keyframes belirme { from { opacity:0; transform:translateY(4px); } to { opacity:1; transform:translateY(0); } }
  .box h2 {
    font-size: 14px; color: #7dd3fc; text-transform: uppercase; margin: 0 0 12px 0; letter-spacing:0.5px;
    display:flex; align-items:center; gap:8px;
  }
  .box h2::before { content:''; width:4px; height:14px; border-radius:2px; background:#7dd3fc; opacity:0.6; flex-shrink:0; }
  .box:hover { border-color:#2c3a5e; }
  .box { transition: border-color .2s; }
  .info-altbaslik {
    font-size: 11.5px; color:#5b6779; text-transform:uppercase; letter-spacing:0.6px;
    margin: 18px 0 8px 0;
  }
  .info-altbaslik:first-of-type { margin-top: 2px; }
  .info-grid {
    display:grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap:10px;
  }
  .info-card {
    background:#0d1326; border:1px solid #1c2540; border-radius:8px;
    padding:10px 12px; display:flex; flex-direction:column; gap:4px;
    transition: border-color .15s, transform .15s;
  }
  .info-card:hover { border-color:#7dd3fc; transform:translateY(-1px); }
  .info-card .k { font-size:10.5px; color:#5b6779; text-transform:uppercase; letter-spacing:0.4px; }
  .info-card .v { font-size:13px; color:#d8dee9; font-weight:bold; word-break:break-word; }
  .info-card .ek { font-size:10.5px; color:#5b6779; }
  .raw-grid { display:flex; flex-direction:column; gap:10px; }
  .raw-item {
    background:#0d1326; border:1px solid #1c2540; border-radius:8px; padding:10px 12px;
  }
  .raw-item .k { display:block; font-size:10.5px; color:#5b6779; text-transform:uppercase; letter-spacing:0.4px; margin-bottom:6px; }
  .raw-item pre {
    margin:0; font-size:12px; color:#a9b4c9; white-space:pre-wrap; word-break:break-word; line-height:1.5;
  }
  a { color: #7dd3fc; }
  code { background:#0b0f1e; padding:1px 5px; border-radius:4px; font-size:12px; }
  @media (max-width: 640px) {
    body { padding: 14px; }
    .topbar { flex-direction:column; align-items:stretch; }
    .toolbar { width:100%; }
    .score-box { flex-direction:column; align-items:flex-start; gap:12px; }
    td, th { padding:7px 6px; font-size:12px; }
  }
  @media print {
    body { background:#fff; color:#000; max-width:none; }
    .logout, .warn a, .toolbar, .navpills, .filterbar, .satir-sayaci { display:none; }
    .box { background:#fff; border:1px solid #999; break-inside: avoid; scroll-margin-top:0; box-shadow:none; animation:none; }
    th { color:#000; }
    .pill { background:none !important; }
    .pill::before { display:none; }
    .durum-iyi, .durum-riskli, .durum-dikkat, .durum-bilinmiyor, .durum-belirsiz, .durum-bilgi { color:#000 !important; }
  }
</style>
</head>
<body>
  <div class="topbar">
    <div>
      <h1>Apache / PHP / MySQL Güvenlik Denetimi</h1>
      <div class="sub">Oluşturulma: <?= h(date('d.m.Y H:i:s')) ?> - İşletim Sistemi: <?= h(PHP_OS) ?> - Belge kökü: <?= h($docRoot) ?></div>
    </div>
    <div class="toolbar">
      <label>
        <input type="checkbox" id="otoYenile"> Oto-yenile (<span id="geriSayim">30</span>sn)
      </label>
      <div class="sep"></div>
      <a class="logout" href="<?= h($_SERVER['PHP_SELF']) ?>">Yenile</a>
      <a class="logout" href="#" onclick="jsonIndir();return false;">JSON indir</a>
      <a class="logout" href="#" onclick="markdownKopyala(this);return false;">Markdown kopyala</a>
      <a class="logout" href="#" onclick="window.print();return false;">Yazdır</a>
      <div class="sep"></div>
      <a class="logout danger" href="?cikis=1">Çıkış yap</a>
    </div>
  </div>

  <div class="warn">
    Bu panel gerçek bir exploit taraması yapmaz; yalnızca sürüm/konfigürasyon/dosya
    tabanlı gösterge sunar. "RİSKLİ" işaretli maddeler için mutlaka
    <a href="https://cve.org" target="_blank" rel="noopener">cve.org</a> veya
    <a href="https://nvd.nist.gov" target="_blank" rel="noopener">nvd.nist.gov</a>
    üzerinden tam CVE listesini kontrol edin ve sürümü güncelleyin.
  </div>

  <?php
    $ringR = 42; $ringC = 2 * M_PI * $ringR;
    $ringOffset = $ringC * (1 - max(0, min(100, $scorePct)) / 100);
    $iyiSayisi = count(array_filter(array_merge($checks, $configChecks), fn($c) => $c['durum'] === 'IYI'));
  ?>
  <div class="score-box">
    <svg class="score-ring" width="100" height="100" viewBox="0 0 100 100">
      <circle class="ring-bg" cx="50" cy="50" r="<?= $ringR ?>"></circle>
      <circle class="ring-fg" cx="50" cy="50" r="<?= $ringR ?>"
        stroke-dasharray="<?= h($ringC) ?>" stroke-dashoffset="<?= h($ringOffset) ?>"></circle>
      <text x="50" y="57" text-anchor="middle"><?= h($scorePct) ?></text>
    </svg>
    <div class="score-label">
      Genel güvenlik skoru: <b><?= h($scoreLabel) ?></b><br>
      <?= h($riskPoints) ?> risk puanı / <?= h($maxPoints) ?> toplam puan üzerinden hesaplandı.
      <div class="stat-chips">
        <span class="stat-chip risk"><span class="dot"></span><?= h($riskliSayisi) ?> RİSKLİ</span>
        <span class="stat-chip uyari"><span class="dot"></span><?= h($dikkatSayisi) ?> DİKKAT</span>
        <span class="stat-chip ok"><span class="dot"></span><?= h($iyiSayisi) ?> İYİ</span>
      </div>
    </div>
  </div>

  <?php $topRiskli = array_slice(array_filter(array_merge($checks, $configChecks), fn($c) => $c['durum'] === 'RISKLI'), 0, 3); ?>
  <?php if ($topRiskli): ?>
  <div class="kritik-box">
    <h3>Önce bunlara bakin</h3>
    <ul>
      <?php foreach ($topRiskli as $kr): ?>
        <li><b><?= h($kr['bilesen']) ?></b> — <?= h($kr['not']) ?></li>
      <?php endforeach; ?>
    </ul>
  </div>
  <?php endif; ?>

  <nav class="navpills">
    <a href="#sürümler">Sürümler</a>
    <a href="#kontroller">Kontroller</a>
    <a href="#ham-çıktı">Ham Çıktı</a>
    <a href="#log">Giriş Logları</a>
  </nav>

  <div class="box" id="sürümler">
    <h2>Sürüm Tabanlı Risk Özeti</h2>
    <div class="table-wrap">
    <table>
      <tr><th>Bileşen</th><th>Durum</th><th>Not</th></tr>
      <?php if (empty($checks)): ?>
        <tr><td colspan="3">Hiçbir bilesen tespit edilemedi (shell_exec kapalı olabilir).</td></tr>
      <?php else: foreach ($checks as $c):
        $cls = 'durum-' . strtolower($c['durum']);
      ?>
        <tr>
          <td><?= h($c['bilesen']) ?></td>
          <td><span class="pill <?= h($cls) ?>"><?= h(durumGoster($c['durum'])) ?></span></td>
          <td><?= h($c['not']) ?></td>
        </tr>
      <?php endforeach; endif; ?>
    </table>
    </div>
  </div>

  <?php $kategoriler = array_values(array_unique(array_column($configChecks, 'kategori'))); sort($kategoriler); ?>
  <div class="box" id="kontroller">
    <h2>Konfigürasyon, Dosya ve Başlık Kontrolleri (<?= h(count($configChecks)) ?> madde)</h2>
    <div class="filterbar">
      <input type="search" id="kontrolAra" placeholder="Ara (ör. .git, xdebug, cron)...">
      <button class="filter-pill" data-durum-filter="" data-active="1">Tümü</button>
      <button class="filter-pill" data-durum-filter="riskli">Riskli</button>
      <button class="filter-pill" data-durum-filter="dikkat">Dikkat</button>
      <button class="filter-pill" data-durum-filter="iyi">İyi</button>
      <button class="filter-pill" data-durum-filter="bilgi">Bilgi</button>
      <select id="kategoriFiltre">
        <option value="">Tüm kategoriler</option>
        <?php foreach ($kategoriler as $kat): ?>
          <option value="<?= h($kat) ?>"><?= h($kat) ?></option>
        <?php endforeach; ?>
      </select>
    </div>
    <div class="satir-sayaci" id="kontrolSayaci"></div>
    <div class="table-wrap">
    <table id="kontrolTablosu">
      <tr><th>Kontrol</th><th>Kategori</th><th>Durum</th><th>Not</th></tr>
      <?php foreach ($configChecks as $c):
        $cls = 'durum-' . strtolower($c['durum']);
      ?>
        <tr data-durum="<?= h(strtolower($c['durum'])) ?>" data-kategori="<?= h($c['kategori']) ?>">
          <td><?= h($c['bilesen']) ?></td>
          <td><span class="kat-etiket"><?= h($c['kategori']) ?></span></td>
          <td><span class="pill <?= h($cls) ?>"><?= h(durumGoster($c['durum'])) ?></span></td>
          <td><?= h($c['not']) ?></td>
        </tr>
      <?php endforeach; ?>
    </table>
    </div>
  </div>

  <?php
    $yazilimBilgisi = [
      'PHP sürümü'          => $phpVersion . ' (' . $phpSapi . ')',
      'Apache sürümü'       => ($apacheVersion && preg_match('/^[\d.]+$/', $apacheVersion)) ? $apacheVersion : 'tespit edilemedi',
      'Nginx sürümü'        => $nv ?: 'tespit edilemedi',
      ($isMariaDb ? 'MariaDB sürümü' : 'mysql-server sürümü') => $mysqlVersion ?: 'tespit edilemedi',
      'OpenSSL'             => $opensslVersion ?: 'tespit edilemedi',
      'cURL'                => $curlVersion ?: 'tespit edilemedi',
      'Xdebug'              => $xdebugVer ?: 'yüklü değil',
      'Composer'            => $cvm[0] ?? 'tespit edilemedi',
      'Node.js'             => $nvm[1] ?? 'tespit edilemedi',
      'npm'                 => $npvm[1] ?? 'tespit edilemedi',
      'Git'                 => $gvm[1] ?? 'tespit edilemedi',
    ];
    $sunucuBilgisi = [
      'İşletim sistemi'       => $osUname,
      'CPU çekirdek sayısı'   => $cpuCores ?: 'tespit edilemedi',
      'Sistem yükü (load avg)' => $yukStr ?: 'tespit edilemedi',
      'RAM (boş / toplam)'    => $ramStr ?: 'tespit edilemedi',
      'Disk (boş / toplam)'   => bayt($diskFree) . ' / ' . bayt($diskTotal) . ($diskYuzde !== null ? " (%$diskYuzde dolu)" : ''),
      'Çalışan işlem sayısı'  => $suanCalisanSurec ?: 'tespit edilemedi',
      'PHP çalıştıran kullanıcı' => $phpCalistiranKullanici ?: 'bilinmiyor',
      'php.ini yolu'          => $phpIniYolu ?: 'bulunamadı',
      'Sunucu yazılımı'       => $serverYazilimi ?: 'bilinmiyor',
      'Sunucu portu'          => ($serverPort ?: 'bilinmiyor') . ($httpsAktifMi ? ' (HTTPS)' : ' (HTTP)'),
      'Sunucu çalışma süresi' => $uptimeStr ?: 'tespit edilemedi',
      'Sunucu adı'            => $hostname,
      'Sunucu IP'             => $serverIp ?: 'bilinmiyor',
      'Belge kökü'            => $docRoot,
      'Zaman dilimi'          => $timezone,
      'memory_limit'          => $memLimit,
      'Yüklü PHP eklentisi'   => count($yukluEklentiler) . ' adet',
    ];
  ?>
  <div class="box" id="ham-çıktı">
    <h2>Sistem Bilgisi</h2>
    <div class="table-wrap">
    <table>
      <tr><th>Bilgi</th><th>Değer</th></tr>
      <tr><td colspan="2" class="tablo-altbaslik">Yazılım Sürümleri</td></tr>
      <?php foreach ($yazilimBilgisi as $k => $v): ?>
        <tr><td><?= h($k) ?></td><td><?= h($v) ?></td></tr>
      <?php endforeach; ?>
      <tr><td colspan="2" class="tablo-altbaslik">Sunucu ve Sistem</td></tr>
      <?php foreach ($sunucuBilgisi as $k => $v): ?>
        <tr><td><?= h($k) ?></td><td><?= h($v) ?></td></tr>
      <?php endforeach; ?>
    </table>
    </div>
  </div>

  <div class="box" id="log">
    <h2>Son Giriş Denemeleri (en yeni en üstte)</h2>
    <div class="table-wrap">
      <table class="log-tablosu">
        <tr><th>Zaman</th><th>Olay</th><th>Sonuç</th><th>IP</th><th>Sebep</th><th>Tarayıcı</th></tr>
        <tbody id="logGovde">
          <tr><td colspan="6" style="color:#5b6779;">Yükleniyor...</td></tr>
        </tbody>
      </table>
    </div>
    <div class="sayfalama" id="logSayfalama"></div>
  </div>

  <script>
    const raporVerisi = <?= json_encode([
        'olusturulma' => date('c'),
        'skor' => $scorePct,
        'skorEtiketi' => $scoreLabel,
        'surumKontrolleri' => $checks,
        'konfigurasyonKontrolleri' => $configChecks,
    ], JSON_UNESCAPED_UNICODE) ?>;

    function markdownKopyala(el) {
      let md = `# Güvenlik Denetim Raporu\n\n`;
      md += `Skor: ${raporVerisi.skor} (${raporVerisi.skorEtiketi}) - ${raporVerisi.olusturulma}\n\n`;
      md += `## Sürüm Tabanli Risk Ozeti\n\n| Bilesen | Durum | Not |\n|---|---|---|\n`;
      raporVerisi.surumKontrolleri.forEach(c => { md += `| ${c.bilesen} | ${c.durum} | ${c.not.replace(/\|/g,'/')} |\n`; });
      md += `\n## Konfigürasyon / Dosya / Ag Kontrolleri\n\n| Bilesen | Kategori | Durum | Not |\n|---|---|---|---|\n`;
      raporVerisi.konfigurasyonKontrolleri.forEach(c => { md += `| ${c.bilesen} | ${c.kategori || ''} | ${c.durum} | ${c.not.replace(/\|/g,'/')} |\n`; });

      navigator.clipboard.writeText(md).then(() => {
        const eski = el.textContent;
        el.textContent = 'Kopyalandi!';
        setTimeout(() => { el.textContent = eski; }, 1500);
      });
    }

    function jsonIndir() {
      const blob = new Blob([JSON.stringify(raporVerisi, null, 2)], {type: 'application/json'});
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'güvenlik-raporu-' + new Date().toISOString().slice(0,19).replace(/[:T]/g,'-') + '.json';
      document.body.appendChild(a);
      a.click();
      a.remove();
    }

    (function () {
      const pills = document.querySelectorAll('.navpills a');
      if (!pills.length) return;
      const hedefler = Array.from(pills).map(p => document.querySelector(p.getAttribute('href'))).filter(Boolean);
      function guncelle() {
        let aktifId = hedefler[0] ? hedefler[0].id : null;
        hedefler.forEach(sec => {
          if (sec.getBoundingClientRect().top - 80 <= 0) aktifId = sec.id;
        });
        pills.forEach(p => {
          if (p.getAttribute('href') === '#' + aktifId) p.setAttribute('data-active', '1');
          else p.removeAttribute('data-active');
        });
      }
      document.addEventListener('scroll', guncelle, { passive: true });
      guncelle();
    })();

    (function () {
      const tablo = document.getElementById('kontrolTablosu');
      if (!tablo) return;
      const satirlar = Array.from(tablo.querySelectorAll('tr[data-durum]'));
      const aramaKutusu = document.getElementById('kontrolAra');
      const filtrePills = document.querySelectorAll('.filter-pill');
      const kategoriSecim = document.getElementById('kategoriFiltre');
      const sayacEl2 = document.getElementById('kontrolSayaci');
      const grup = { dikkat: ['dikkat', 'bilinmiyor', 'belirsiz'] };
      let aktifFiltre = '';

      function uygula() {
        const q = (aramaKutusu.value || '').toLowerCase().trim();
        const kat = kategoriSecim ? kategoriSecim.value : '';
        let gosterilen = 0;
        satirlar.forEach(tr => {
          const durum = tr.getAttribute('data-durum') || '';
          const kategori = tr.getAttribute('data-kategori') || '';
          const metin = tr.textContent.toLowerCase();
          const durumUyar = !aktifFiltre || durum === aktifFiltre || (grup[aktifFiltre] || []).includes(durum);
          const katUyar = !kat || kategori === kat;
          const metinUyar = !q || metin.includes(q);
          const goster = durumUyar && katUyar && metinUyar;
          tr.style.display = goster ? '' : 'none';
          if (goster) gosterilen++;
        });
        sayacEl2.textContent = gosterilen + ' / ' + satirlar.length + ' madde gosteriliyor';
      }

      aramaKutusu.addEventListener('input', uygula);
      if (kategoriSecim) kategoriSecim.addEventListener('change', uygula);
      filtrePills.forEach(p => {
        p.addEventListener('click', () => {
          filtrePills.forEach(x => x.removeAttribute('data-active'));
          p.setAttribute('data-active', '1');
          aktifFiltre = p.getAttribute('data-durum-filter') || '';
          uygula();
        });
      });
      uygula();
    })();

    (function () {
      const govde = document.getElementById('logGovde');
      const sayfalamaEl = document.getElementById('logSayfalama');
      const kutuEl = document.querySelector('#log .table-wrap');
      if (!govde) return;

      const OLAY_ETIKET = {
        GIRIS: 'giriş',
        KILIT: 'kilit',
        IP_ENGEL: 'ip engeli',
        OTURUM: 'oturum',
        CIKIS: 'çıkış',
      };
      const OLAY_SINIF = {
        GIRIS: 'olay-giris',
        KILIT: 'olay-kilit',
        IP_ENGEL: 'olay-kilit',
        OTURUM: 'olay-notr',
        CIKIS: 'olay-notr',
      };

      let aktifSayfa = 1;
      let yükleniyor = false;
      let istekNo = 0;

      function kacir(s) {
        const d = document.createElement('div');
        d.textContent = s ?? '';
        return d.innerHTML;
      }

      function satirCiz(p) {
        const tr = document.createElement('tr');
        if (p[0] === null) {
          tr.innerHTML = '<td colspan="6">' + kacir(p[5]) + '</td>';
          return tr;
        }
        const pillCls = p[2] === 'BASARILI' ? 'durum-iyi' : (p[2] === 'BASARISIZ' ? 'durum-riskli' : 'durum-bilgi');
        const olaySinif = OLAY_SINIF[p[1]] || 'olay-notr';
        const olayMetin = OLAY_ETIKET[p[1]] || p[1];
        const SONUC_GOSTER = { BASARILI: 'BAŞARILI', BASARISIZ: 'BAŞARISIZ' };
        const sonucMetin = SONUC_GOSTER[p[2]] || p[2];
        tr.innerHTML = '<td>' + kacir(p[0]) + '</td>'
          + '<td><span class="olay-rozet ' + olaySinif + '">' + kacir(olayMetin) + '</span></td>'
          + '<td><span class="pill ' + pillCls + '">' + kacir(sonucMetin) + '</span></td>'
          + '<td>' + kacir(p[3]) + '</td>'
          + '<td>' + kacir(p[4]) + '</td>'
          + '<td class="log-ua" title="' + kacir(p[5]) + '">' + kacir(p[5]) + '</td>';
        return tr;
      }

      function sayfaYukle(sayfa) {
        if (yükleniyor) return;
        yükleniyor = true;
        const buIstek = ++istekNo;
        aktifSayfa = sayfa;
        if (kutuEl) kutuEl.classList.add('yükleniyor-durum');
        sayfalamaEl.querySelectorAll('button').forEach(b => b.disabled = true);

        fetch('?ajax=log&sayfa=' + encodeURIComponent(sayfa))
          .then(r => r.json())
          .then(veri => {
            if (buIstek !== istekNo) return; // eski istek gecikti, yoksay
            govde.innerHTML = '';
            if (!veri.satirlar.length) {
              govde.innerHTML = '<tr><td colspan="6" style="color:#8891a8;">Kayıt yok. Sunucuda acik_giris.log dosyasını oluşturup 777 izin verin.</td></tr>';
            } else {
              veri.satirlar.forEach(p => govde.appendChild(satirCiz(p)));
            }
            sayfalamaCiz(veri.sayfa, veri.toplamSayfa, veri.toplam);
          })
          .catch(() => {
            if (buIstek !== istekNo) return;
            govde.innerHTML = '<tr><td colspan="6" style="color:#ff6b6b;">Log yüklenemedi. Tekrar deneyin.</td></tr>';
          })
          .finally(() => {
            if (buIstek !== istekNo) return;
            yükleniyor = false;
            if (kutuEl) kutuEl.classList.remove('yükleniyor-durum');
          });
      }

      function sayfaDugmesi(etiket, hedefSayfa, aktifMi, devreDisiMi) {
        const b = document.createElement('button');
        b.type = 'button';
        b.textContent = etiket;
        b.className = 'sayfa-btn' + (aktifMi ? ' aktif' : '');
        b.disabled = !!devreDisiMi;
        if (!devreDisiMi) b.addEventListener('click', () => sayfaYukle(hedefSayfa));
        return b;
      }

      function ellipsis() {
        const s = document.createElement('span');
        s.className = 'sayfa-ellipsis';
        s.textContent = '...';
        return s;
      }

      function sayfalamaCiz(aktif, toplamSayfa, toplam) {
        sayfalamaEl.innerHTML = '';
        const bilgi = document.createElement('span');
        bilgi.className = 'sayfa-bilgi';
        bilgi.textContent = 'Toplam ' + toplam + ' kayıt' + (toplamSayfa > 1 ? ' - sayfa ' + aktif + '/' + toplamSayfa : '');
        sayfalamaEl.appendChild(bilgi);
        if (toplamSayfa <= 1) return;

        sayfalamaEl.appendChild(sayfaDugmesi('‹', aktif - 1, false, aktif <= 1));

        const pencere = 2;
        const gosterilecek = new Set([1, toplamSayfa]);
        for (let i = aktif - pencere; i <= aktif + pencere; i++) {
          if (i >= 1 && i <= toplamSayfa) gosterilecek.add(i);
        }
        const siraliSayfalar = Array.from(gosterilecek).sort((a, b) => a - b);
        let oncekiSayfa = 0;
        siraliSayfalar.forEach(sayfaNo => {
          if (oncekiSayfa && sayfaNo - oncekiSayfa > 1) sayfalamaEl.appendChild(ellipsis());
          sayfalamaEl.appendChild(sayfaDugmesi(sayfaNo, sayfaNo, sayfaNo === aktif, false));
          oncekiSayfa = sayfaNo;
        });

        sayfalamaEl.appendChild(sayfaDugmesi('›', aktif + 1, false, aktif >= toplamSayfa));
      }

      sayfaYukle(1);
    })();

    const kutu = document.getElementById('otoYenile');
    const sayacEl = document.getElementById('geriSayim');
    const KEY = 'acikOtoYenile';
    let saniye = 30;
    let zamanlayici = null;

    function baslat() {
      saniye = 30;
      sayacEl.textContent = saniye;
      zamanlayici = setInterval(() => {
        saniye--;
        sayacEl.textContent = saniye;
        if (saniye <= 0) location.reload();
      }, 1000);
    }
    function durdur() {
      clearInterval(zamanlayici);
      sayacEl.textContent = '30';
    }

    kutu.addEventListener('change', () => {
      localStorage.setItem(KEY, kutu.checked ? '1' : '0');
      kutu.checked ? baslat() : durdur();
    });

    if (localStorage.getItem(KEY) === '1') {
      kutu.checked = true;
      baslat();
    }
  </script>
</body>
</html>
💬 Yorum Bırak