Web Site Pentester (Web Site Güvenlik Analiz Aracı) / SitePenTest/scanner.py
scanner.py 402 satır • 19.03 KB
# scanner.py - SitePenTest v1.1
import requests
import os
import time
import random
import html as html_lib  # Çakışmayı önlemek için isimlendirdik
from urllib.parse import urljoin, urlparse, unquote
from datetime import datetime
import urllib3
import ssl
import socket
from bs4 import BeautifulSoup

urllib3.disable_warnings()


class WebScanner:
    def __init__(self, target_url):
        self.target = self.clean_url(target_url)
        self.domain = urlparse(self.target).netloc
        self.session = requests.Session()

        # WAF Bypass: Rastgele User-Agent Havuzu
        self.user_agents = [
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
        ]

        self.findings = []
        self.goods = []
        self.checked_features = []

    def clean_url(self, url):
        if not url.startswith(("http://", "https://")):
            url = "https://" + url
        return url.rstrip("/")

    def get_random_header(self):
        return {"User-Agent": random.choice(self.user_agents)}

    def request(self, url, method="GET", timeout=8, data=None, allow_redirects=True):
        """WAF dostu, gecikmeli ve random header'lı istek atar"""
        try:
            # WAF'ı tetiklememek için rastgele kısa bekleme (0.3 - 0.8 sn)
            time.sleep(random.uniform(0.3, 0.8))

            headers = self.get_random_header()
            return self.session.request(
                method,
                url,
                headers=headers,
                timeout=timeout,
                allow_redirects=allow_redirects,
                verify=False,
                data=data
            )
        except Exception:
            return None

    def add_finding(self, title, risk, desc, exploit, fix, owasp=""):
        self.findings.append({
            "title": title, "risk": risk, "desc": desc,
            "exploit": exploit, "fix": fix, "owasp": owasp
        })

    def add_good(self, msg):
        self.goods.append(msg)

    def add_checked_feature(self, feature):
        self.checked_features.append(feature)

    def scan(self, progress_callback=None):
        steps = 10
        current = 0

        # Adımları gruplayarak progress barı daha akıcı yaptık
        scan_groups = [
            (self.scan_critical_paths, "Kritik dosyalar ve Dizinler"),
            (self.scan_security_headers, "Güvenlik Header Analizi"),
            (self.scan_cookies, "Cookie Yapılandırması"),
            (self.scan_server_leak, "Sunucu Bilgi Sızıntıları"),
            (self.scan_robots, "Robots.txt ve Security.txt"),
            (self.scan_ssl, "SSL/TLS Şifreleme"),
            (self.scan_cors, "CORS Yapılandırması"),
            (self.scan_outdated, "Versiyon Kontrolleri"),
            (self.scan_basic_vuln, "XSS ve SQL Enjeksiyon Testleri (WAF Bypass Modu)"),
            (self.scan_csrf_and_session, "CSRF ve Oturum Güvenliği")
        ]

        for func, msg in scan_groups:
            current += 1
            if progress_callback:
                progress_callback(current / steps * 100, f"{msg} taranıyor...")
            func()

        progress_callback(100, "Rapor derleniyor...")

    # --- GELİŞMİŞ TARAMA FONKSİYONLARI ---

    def scan_critical_paths(self):
        self.add_checked_feature("Kritik Dosya Taraması")
        paths = ['/.env', '/wp-config.php', '/.git/HEAD', '/backup.sql', '/phpinfo.php']
        for p in paths:
            resp = self.request(urljoin(self.target, p))
            if resp and resp.status_code == 200:
                # False Positive Önleme: İçerik kontrolü
                content = resp.text.lower()
                is_real = False

                if ".env" in p and ("db_password" in content or "app_key" in content):
                    is_real = True
                elif "wp-config" in p and "db_name" in content:
                    is_real = True
                elif ".git" in p and "ref: refs/" in content:
                    is_real = True
                elif "phpinfo" in p and "php version" in content:
                    is_real = True

                if is_real:
                    self.add_finding("Kritik Dosya İfşası", "KRİTİK", f"{p} dosyası okunabiliyor.",
                                     "Veritabanı şifreleri çalınabilir.",
                                     "Dosyayı sunucudan silin veya erişimi kapatın.", "A05")

    def scan_security_headers(self):
        self.add_checked_feature("Header Analizi")
        resp = self.request(self.target)
        if not resp: return
        h = {k.lower(): v for k, v in resp.headers.items()}

        required = {
            'strict-transport-security': ("HSTS Eksik", "YÜKSEK"),
            'content-security-policy': ("CSP Eksik", "ORTA"),
            'x-frame-options': ("Clickjacking Koruması Yok", "ORTA")
        }

        for head, (title, risk) in required.items():
            if head not in h:
                self.add_finding(title, risk, f"{head} header'ı sunucudan dönmedi.",
                                 "Tarayıcı tabanlı korumalar devre dışı.", f"{head} header'ını ekleyin.", "A05")
            else:
                self.add_good(f"{head} aktif")

    def scan_cookies(self):
        self.add_checked_feature("Cookie Güvenliği")
        resp = self.request(self.target)
        if not resp or 'set-cookie' not in resp.headers: return

        cookies = resp.headers['set-cookie'].lower()
        if 'httponly' not in cookies:
            self.add_finding("HttpOnly Flag Eksik", "ORTA", "Cookie'lere JS ile erişilebilir.",
                             "XSS saldırılarında session çalınabilir.", "HttpOnly=True yapın.", "A03")
        if 'secure' not in cookies and self.target.startswith("https"):
            self.add_finding("Secure Flag Eksik", "DÜŞÜK", "Cookie HTTP üzerinden gidebilir.",
                             "MITM saldırısı riski.", "Secure=True yapın.", "A05")

    def scan_server_leak(self):
        self.add_checked_feature("Bilgi Sızıntısı")
        resp = self.request(self.target)
        if not resp: return
        h = {k.lower(): v for k, v in resp.headers.items()}

        if 'server' in h:
            val = h['server']
            # Sadece versiyon numarası varsa uyar (örn: Apache/2.4.49)
            if any(char.isdigit() for char in val):
                self.add_finding("Sunucu Versiyon İfşası", "DÜŞÜK", f"Server: {val}",
                                 "Saldırgan spesifik exploit arayabilir.",
                                 "ServerTokens Prod (Apache) veya hide_server_tokens (Nginx)", "A05")

    def scan_robots(self):
        self.add_checked_feature("Robots.txt")
        resp = self.request(urljoin(self.target, "/robots.txt"))
        if resp and resp.status_code == 200:
            if "admin" in resp.text:
                self.add_finding("robots.txt Hassas Bilgi", "DÜŞÜK", "Admin paneli yolu robots.txt'de ifşa edilmiş.",
                                 "Saldırganlar gizli panelleri bulabilir.", "Hassas yolları robots.txt'ye yazmayın.",
                                 "A01")
            self.add_good("robots.txt mevcut")
        else:
            self.add_finding("robots.txt Bulunamadı", "DÜŞÜK", "Dosya 404 dönüyor.", "Bot kontrolü yapılamıyor.",
                             "Dosyayı oluşturun.", "A05")

    def scan_ssl(self):
        self.add_checked_feature("SSL/TLS")
        try:
            ctx = ssl.create_default_context()
            with socket.create_connection((self.domain, 443), timeout=5) as sock:
                with ctx.wrap_socket(sock, server_hostname=self.domain) as ssock:
                    ver = ssock.version()
                    if ver in ["TLSv1", "TLSv1.1"]:
                        self.add_finding("Zayıf TLS Protokolü", "YÜKSEK", f"{ver} kullanılıyor.",
                                         "POODLE vb. saldırılara açık.", "Sadece TLS 1.2 ve 1.3'e izin verin.", "A02")
                    else:
                        self.add_good(f"Güvenli Protokol: {ver}")
        except:
            pass  # Bağlantı hatası raporlamıyoruz, zaten request çalışmazsa belli olur.

    def scan_cors(self):
        self.add_checked_feature("CORS")
        resp = self.request(self.target, method="OPTIONS")
        if resp and 'access-control-allow-origin' in resp.headers:
            if resp.headers['access-control-allow-origin'] == '*':
                self.add_finding("Gevşek CORS Politikası", "ORTA", "Access-Control-Allow-Origin: *",
                                 "Herkes API'ye erişebilir.", "Origin'i spesifik domainlerle sınırlayın.", "A07")

    def scan_outdated(self):
        self.add_checked_feature("Yazılım Versiyonları")
        # Basit header kontrolü, derinlemesine analiz için Wappalyzer gerekir ama bu temel bir kontrol.
        pass

    def scan_basic_vuln(self):
        self.add_checked_feature("Enjeksiyon Testleri")

        # 1. Reflected XSS (False Positive Korumalı)
        # Benzersiz bir string kullanıyoruz ki sayfada zaten var olan "alert" kelimesiyle karışmasın.
        unique_str = "XSS_TEST_RND_99"
        payload = f"<script>console.log('{unique_str}')</script>"
        test_url = urljoin(self.target, f"?search={payload}")

        resp = self.request(test_url)
        if resp:
            # Payload sayfada var mı VE escape edilmemiş mi? (örn: &lt;script&gt; olarak dönmemeli)
            if payload in resp.text:
                self.add_finding("Reflected XSS", "YÜKSEK", "JS kodu sayfada çalıştırılabilir halde yansıyor.",
                                 "Kullanıcı oturumu çalınabilir.", "Input'ları HTML Encode yapın.", "A03")

        # 2. SQL Injection (Hata Bazlı - False Positive Korumalı)
        # Normal request
        resp_norm = self.request(self.target)
        len_norm = len(resp_norm.text) if resp_norm else 0

        # SQL Payload
        sql_payload = "' OR '1'='1"
        test_url_sql = urljoin(self.target, f"?id={sql_payload}")
        resp_sql = self.request(test_url_sql)

        if resp_sql:
            errors = ["sql syntax", "mysql_fetch", "ora-01756", "syntax error"]
            found_error = any(e in resp_sql.text.lower() for e in errors)

            # Eğer normal sayfada bu hata yoksa VE sql payload ile hata çıkıyorsa
            if found_error and (resp_norm and not any(e in resp_norm.text.lower() for e in errors)):
                self.add_finding("SQL Injection (Error Based)", "KRİTİK", "Veritabanı hata mesajı tetiklendi.",
                                 "Tüm veritabanı okunabilir.", "Prepared Statements kullanın.", "A03")

    def scan_csrf_and_session(self):
        self.add_checked_feature("CSRF & Session")
        resp = self.request(self.target)
        if resp:
            soup = BeautifulSoup(resp.text, 'html.parser')
            forms = soup.find_all('form')
            for form in forms:
                # Login veya update formu mu?
                action = form.get('action', '').lower()
                if 'login' in action or 'register' in action or 'update' in action:
                    # Hidden inputlarda csrf, token vb. ara
                    inputs = form.find_all('input', type='hidden')
                    has_token = any(
                        x.get('name') and ('csrf' in x.get('name').lower() or 'token' in x.get('name').lower()) for x in
                        inputs)

                    if not has_token:
                        self.add_finding("CSRF Token Eksik", "ORTA", "Kritik formda CSRF token bulunamadı.",
                                         "Kullanıcı adına habersiz işlem yapılabilir.",
                                         "Formlara anti-CSRF token ekleyin.", "A01")
                        break  # Bir tane bulsak yeter

    def generate_report(self):
        os.makedirs("reports", exist_ok=True)
        filename = f"reports/Rapor_{self.domain.replace('.', '_')}.html"

        risk_score = 100 - (len(self.findings) * 10)
        if risk_score < 0: risk_score = 0

        # --- HTML & CSS (Print-Friendly & PDF Ready) ---
        html_content = f"""
        <!DOCTYPE html>
        <html lang="tr">
        <head>
            <meta charset="UTF-8">
            <title>Güvenlik Raporu - {self.domain}</title>
            <style>
                @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');

                :root {{ --primary: #2563eb; --danger: #dc2626; --warning: #d97706; --success: #16a34a; --bg: #f8fafc; }}

                body {{ font-family: 'Inter', sans-serif; background: var(--bg); color: #1e293b; margin: 0; padding: 40px; line-height: 1.5; }}

                /* EKRAN GÖRÜNÜMÜ */
                .container {{ max-width: 900px; margin: 0 auto; background: white; padding: 40px; border-radius: 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.05); }}

                h1 {{ font-size: 28px; margin-bottom: 5px; color: #0f172a; }}
                .meta {{ color: #64748b; font-size: 14px; margin-bottom: 30px; border-bottom: 2px solid #e2e8f0; padding-bottom: 20px; }}

                .score-card {{ display: flex; justify-content: space-between; align-items: center; background: #f1f5f9; padding: 20px; border-radius: 12px; margin-bottom: 30px; }}
                .score {{ font-size: 32px; font-weight: bold; color: {"#16a34a" if risk_score > 70 else "#dc2626"}; }}

                .finding {{ border: 1px solid #e2e8f0; border-radius: 8px; padding: 15px; margin-bottom: 15px; page-break-inside: avoid; }}
                .finding.KRİTİK {{ border-left: 5px solid var(--danger); }}
                .finding.YÜKSEK {{ border-left: 5px solid #f97316; }}
                .finding.ORTA {{ border-left: 5px solid var(--warning); }}
                .finding.DÜŞÜK {{ border-left: 5px solid #94a3b8; }}

                .badge {{ display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: bold; color: white; margin-bottom: 8px; }}

                .btn-print {{ position: fixed; top: 20px; right: 20px; background: var(--primary); color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; box-shadow: 0 4px 10px rgba(37,99,235,0.3); }}
                .btn-print:hover {{ background: #1d4ed8; }}

                /* PDF / YAZICI AYARLARI (TEK SAYFAYA SIĞDIRMA ÇABASI) */
                @media print {{
                    @page {{ margin: 10mm; size: A4; }}
                    body {{ background: white; padding: 0; font-size: 11px; -webkit-print-color-adjust: exact; }}
                    .container {{ box-shadow: none; max-width: 100%; padding: 0; }}
                    .btn-print {{ display: none; }}
                    h1 {{ font-size: 20px; margin-top: 0; }}
                    .meta {{ margin-bottom: 10px; padding-bottom: 10px; }}
                    .score-card {{ padding: 10px; margin-bottom: 15px; }}
                    .score {{ font-size: 24px; }}

                    /* Grid yapısı ile bulguları yan yana dizerek yerden tasarruf et */
                    .findings-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }}
                    .finding {{ margin-bottom: 0; padding: 10px; border: 1px solid #ccc; }}

                    /* Gereksiz detayları kısalt */
                    p {{ margin: 4px 0; }}

                    /* Kritik olmayan bölümleri gizleyerek tek sayfaya sığdırabiliriz (Opsiyonel) */
                    .footer {{ display: none; }}
                }}
            </style>
        </head>
        <body>
            <button class="btn-print" onclick="window.print()">🖨️ PDF Olarak Kaydet / Yazdır</button>

            <div class="container">
                <div style="display:flex; justify-content:space-between; align-items:center;">
                    <div>
                        <h1>🛡️ Güvenlik Tarama Raporu</h1>
                        <div class="meta">
                            Hedef: <b>{self.target}</b><br>
                            Tarih: {datetime.now().strftime('%d.%m.%Y %H:%M')}<br>
                            Tarayıcı: SitePenTest v1.1
                        </div>
                    </div>
                    <div style="text-align:right;">
                        <span style="font-size:40px;">{risk_score}/100</span><br>
                        <span style="color:#64748b; font-size:12px;">Güvenlik Skoru</span>
                    </div>
                </div>

                <div class="score-card">
                    <div>
                        <strong>Tespitler:</strong> {len(self.findings)} Riskli Bulgu
                    </div>
                    <div>
                        <span style="color:#22c55e;">✓ {len(self.goods)} Güvenli Nokta</span>
                    </div>
                </div>

                <h3 style="border-bottom: 2px solid #e2e8f0; padding-bottom: 5px;">⚠️ Tespit Edilen Riskler</h3>
                <div class="findings-grid">
        """

        for f in self.findings:
            bg_color = "#dc2626" if f['risk'] == "KRİTİK" else "#f97316" if f['risk'] == "YÜKSEK" else "#eab308" if f[
                                                                                                                        'risk'] == "ORTA" else "#94a3b8"
            html_content += f"""
            <div class="finding {f['risk']}">
                <span class="badge" style="background:{bg_color}">{f['risk']}</span>
                <strong style="display:block; font-size:13px; margin-bottom:5px;">{f['title']}</strong>
                <p style="font-size:11px; color:#475569;">{f['desc']}</p>
                <div style="background:#f1f5f9; padding:5px; border-radius:4px; margin-top:5px; font-size:10px; font-family:monospace;">
                    <strong>Çözüm:</strong> {f['fix']}
                </div>
            </div>
            """

        if not self.findings:
            html_content += """<div style="grid-column: span 2; text-align:center; padding: 40px; background:#f0fdf4; border-radius:10px; color:#16a34a;">
            <h3>Harika!</h3><p>Otomatik taramada kritik bir zafiyete rastlanmadı.</p></div>"""

        html_content += f"""
                </div> <h3 style="margin-top:30px; border-bottom: 2px solid #e2e8f0; padding-bottom: 5px;">✅ Kontrol Edilen Alanlar</h3>
                <ul style="font-size:11px; color:#64748b; columns: 2;">
                    {''.join([f'<li>{item}</li>' for item in self.checked_features])}
                </ul>

                <div class="footer" style="text-align:center; margin-top:40px; font-size:10px; color:#cbd5e1;">
                    Bu rapor otomatik oluşturulmuştur. Kesin güvenlik garantisi vermez.<br>
                    Generated by SitePenTest v1.1
                </div>
            </div>
        </body>
        </html>
        """

        with open(filename, "w", encoding="utf-8") as f:
            f.write(html_content)

        return filename