# 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: <script> 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