mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
feat: add passive pentest scanner script with PEP 723 uv metadata
This commit is contained in:
@@ -1429,6 +1429,19 @@
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: osltsstu c8c1516d "fix: validation error messages hidden by generic fallback in ErrorHandler::userMessage" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: osltsstu c8c1516d "fix: validation error messages hidden by generic fallback in ErrorHandler::userMessage" (rebased revision)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
- $linkName = $link['name'] ?? '';
|
||||
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: srupvzpw 6bfa45fc "feat: add passive pentest scanner script with PEP 723 uv metadata" (rebased revision)
|
||||
$linkName = $link['name'] ?? '';
|
||||
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
$linkLockedYear = $link['locked_year'] ?? null;
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: srupvzpw 5a2ec7e2 "feat: add passive pentest scanner script with PEP 723 uv metadata" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
?>
|
||||
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">
|
||||
|
||||
469
pentest_xamxam.py
Normal file
469
pentest_xamxam.py
Normal file
@@ -0,0 +1,469 @@
|
||||
#!/usr/bin/env -S uv run
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "requests",
|
||||
# "dnspython",
|
||||
# "colorama",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Passive / Non-Destructive Pentest Script
|
||||
Target: xamxam.erg.be
|
||||
Author: generated for site owner / authorized use only
|
||||
|
||||
Checks performed:
|
||||
1. DNS & WHOIS metadata
|
||||
2. TLS certificate info
|
||||
3. HTTP security headers audit
|
||||
4. Cookie flags audit
|
||||
5. Sensitive file/path exposure
|
||||
6. robots.txt & sitemap.xml parsing
|
||||
7. Open redirect probe (non-exploiting)
|
||||
8. Reflected XSS canary in search param (detection only)
|
||||
9. Directory listing detection
|
||||
10. Software version disclosure in headers/meta
|
||||
11. CORS misconfiguration check
|
||||
12. Clickjacking (X-Frame-Options / CSP frame-ancestors)
|
||||
13. HSTS check
|
||||
14. Subdomain enumeration (wordlist)
|
||||
15. Summary report
|
||||
|
||||
Usage:
|
||||
pip install requests dnspython colorama
|
||||
python3 pentest_xamxam.py
|
||||
"""
|
||||
|
||||
import socket
|
||||
import ssl
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
import urllib.parse
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
import dns.resolver
|
||||
from colorama import Fore, Style, init
|
||||
|
||||
init(autoreset=True)
|
||||
|
||||
# ── Config ────────────────────────────────────────────────────────────────────
|
||||
TARGET_HOST = "xamxam.erg.be"
|
||||
TARGET_BASE = f"https://{TARGET_HOST}"
|
||||
TIMEOUT = 8
|
||||
HEADERS = {"User-Agent": "PentestScanner/1.0 (authorized audit)"}
|
||||
|
||||
# Paths an attacker would probe first
|
||||
SENSITIVE_PATHS = [
|
||||
"/.env", "/.env.local", "/.env.production",
|
||||
"/.git/HEAD", "/.git/config",
|
||||
"/wp-admin", "/wp-login.php", # CMS probes
|
||||
"/admin", "/administrator", "/admin/login",
|
||||
"/phpmyadmin", "/pma",
|
||||
"/backup.zip", "/backup.sql", "/db.sql",
|
||||
"/config.php", "/config.json", "/settings.py",
|
||||
"/robots.txt", "/sitemap.xml",
|
||||
"/.htaccess", "/web.config",
|
||||
"/api", "/api/v1", "/api/users",
|
||||
"/graphql",
|
||||
"/server-status", # Apache status page
|
||||
"/actuator", "/actuator/health", # Spring Boot
|
||||
"/_next/static", # Next.js artefacts
|
||||
"/static",
|
||||
"/uploads", "/files", "/media",
|
||||
]
|
||||
|
||||
SECURITY_HEADERS = {
|
||||
"Strict-Transport-Security": "HSTS — forces HTTPS",
|
||||
"Content-Security-Policy": "CSP — mitigates XSS",
|
||||
"X-Frame-Options": "Clickjacking protection",
|
||||
"X-Content-Type-Options": "MIME sniffing protection",
|
||||
"Referrer-Policy": "Controls referrer leakage",
|
||||
"Permissions-Policy": "Limits browser feature access",
|
||||
"X-XSS-Protection": "Legacy XSS filter (deprecated but informative)",
|
||||
"Cross-Origin-Opener-Policy": "Isolates browsing context",
|
||||
"Cross-Origin-Resource-Policy": "Controls cross-origin resource sharing",
|
||||
}
|
||||
|
||||
SUBDOMAIN_WORDLIST = [
|
||||
"www", "mail", "smtp", "ftp", "dev", "staging", "test", "api",
|
||||
"admin", "portal", "old", "beta", "cdn", "static", "media",
|
||||
"docs", "blog", "intranet", "vpn", "login", "auth",
|
||||
]
|
||||
|
||||
XSS_CANARY = "<script>alert(1)</script>"
|
||||
OPEN_REDIRECT_PAYLOADS = ["//evil.com", "https://evil.com", "/\\evil.com"]
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
def ok(msg): print(f" {Fore.GREEN}[✓]{Style.RESET_ALL} {msg}")
|
||||
def warn(msg): print(f" {Fore.YELLOW}[!]{Style.RESET_ALL} {msg}")
|
||||
def bad(msg): print(f" {Fore.RED}[✗]{Style.RESET_ALL} {msg}")
|
||||
def info(msg): print(f" {Fore.CYAN}[i]{Style.RESET_ALL} {msg}")
|
||||
def section(title):
|
||||
print(f"\n{Fore.MAGENTA}{'═'*60}{Style.RESET_ALL}")
|
||||
print(f"{Fore.MAGENTA} {title}{Style.RESET_ALL}")
|
||||
print(f"{Fore.MAGENTA}{'═'*60}{Style.RESET_ALL}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Finding:
|
||||
severity: str # CRITICAL / HIGH / MEDIUM / LOW / INFO
|
||||
title: str
|
||||
detail: str
|
||||
|
||||
|
||||
findings: list[Finding] = []
|
||||
|
||||
def add(severity, title, detail=""):
|
||||
findings.append(Finding(severity, title, detail))
|
||||
|
||||
|
||||
def get(path="", params=None, allow_redirects=True) -> Optional[requests.Response]:
|
||||
try:
|
||||
return requests.get(
|
||||
TARGET_BASE + path,
|
||||
params=params,
|
||||
headers=HEADERS,
|
||||
timeout=TIMEOUT,
|
||||
allow_redirects=allow_redirects,
|
||||
verify=True,
|
||||
)
|
||||
except requests.exceptions.SSLError as e:
|
||||
add("HIGH", f"SSL error on {path}", str(e))
|
||||
return None
|
||||
except requests.exceptions.ConnectionError:
|
||||
return None
|
||||
except requests.exceptions.Timeout:
|
||||
return None
|
||||
|
||||
|
||||
# ── 1. DNS & IP ───────────────────────────────────────────────────────────────
|
||||
def check_dns():
|
||||
section("1. DNS & IP Resolution")
|
||||
try:
|
||||
ip = socket.gethostbyname(TARGET_HOST)
|
||||
info(f"A record → {ip}")
|
||||
add("INFO", "A record", ip)
|
||||
except Exception as e:
|
||||
bad(f"DNS resolution failed: {e}")
|
||||
|
||||
for rtype in ("MX", "TXT", "NS", "AAAA"):
|
||||
try:
|
||||
answers = dns.resolver.resolve(TARGET_HOST, rtype)
|
||||
for r in answers:
|
||||
info(f"{rtype:5s} → {r}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── 2. TLS Certificate ────────────────────────────────────────────────────────
|
||||
def check_tls():
|
||||
section("2. TLS Certificate")
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
with ctx.wrap_socket(socket.socket(), server_hostname=TARGET_HOST) as s:
|
||||
s.connect((TARGET_HOST, 443))
|
||||
cert = s.getpeercert()
|
||||
|
||||
subject = dict(x[0] for x in cert["subject"])
|
||||
issuer = dict(x[0] for x in cert["issuer"])
|
||||
not_after = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
|
||||
days_left = (not_after.replace(tzinfo=datetime.timezone.utc) - datetime.datetime.now(datetime.UTC)).days
|
||||
|
||||
info(f"Subject : {subject.get('commonName')}")
|
||||
info(f"Issuer : {issuer.get('organizationName')}")
|
||||
info(f"Expires : {not_after.date()} ({days_left} days)")
|
||||
|
||||
sans = [v for t, v in cert.get("subjectAltName", []) if t == "DNS"]
|
||||
info(f"SANs : {', '.join(sans)}")
|
||||
|
||||
if days_left < 30:
|
||||
bad(f"Certificate expires in {days_left} days!")
|
||||
add("HIGH", "Certificate near expiry", f"{days_left} days remaining")
|
||||
else:
|
||||
ok("Certificate valid and not near expiry")
|
||||
|
||||
except Exception as e:
|
||||
bad(f"TLS check failed: {e}")
|
||||
add("HIGH", "TLS check failed", str(e))
|
||||
|
||||
|
||||
# ── 3. Security Headers ───────────────────────────────────────────────────────
|
||||
def check_headers():
|
||||
section("3. HTTP Security Headers")
|
||||
r = get("/")
|
||||
if not r:
|
||||
bad("Could not reach target"); return
|
||||
|
||||
# Show server / technology leakage
|
||||
for leak_header in ("Server", "X-Powered-By", "X-AspNet-Version", "X-Generator"):
|
||||
val = r.headers.get(leak_header)
|
||||
if val:
|
||||
warn(f"Version disclosure — {leak_header}: {val}")
|
||||
add("LOW", f"Version disclosure: {leak_header}", val)
|
||||
|
||||
for header, description in SECURITY_HEADERS.items():
|
||||
val = r.headers.get(header)
|
||||
if val:
|
||||
ok(f"{header}: {val}")
|
||||
else:
|
||||
bad(f"Missing: {header} ({description})")
|
||||
severity = "HIGH" if header in ("Strict-Transport-Security", "Content-Security-Policy", "X-Frame-Options") else "MEDIUM"
|
||||
add(severity, f"Missing header: {header}", description)
|
||||
|
||||
|
||||
# ── 4. Cookie Flags ───────────────────────────────────────────────────────────
|
||||
def check_cookies():
|
||||
section("4. Cookie Security Flags")
|
||||
r = get("/")
|
||||
if not r: return
|
||||
|
||||
raw_cookies = r.headers.get("Set-Cookie", "")
|
||||
if not raw_cookies:
|
||||
info("No Set-Cookie header on homepage (may appear after login)")
|
||||
return
|
||||
|
||||
for cookie_str in raw_cookies.split(","):
|
||||
name = cookie_str.split("=")[0].strip()
|
||||
flags = cookie_str.lower()
|
||||
issues = []
|
||||
if "httponly" not in flags:
|
||||
issues.append("missing HttpOnly")
|
||||
if "secure" not in flags:
|
||||
issues.append("missing Secure")
|
||||
if "samesite" not in flags:
|
||||
issues.append("missing SameSite")
|
||||
if issues:
|
||||
warn(f"Cookie '{name}': {', '.join(issues)}")
|
||||
add("MEDIUM", f"Cookie flags: {name}", "; ".join(issues))
|
||||
else:
|
||||
ok(f"Cookie '{name}' has all recommended flags")
|
||||
|
||||
|
||||
# ── 5. Sensitive File Exposure ────────────────────────────────────────────────
|
||||
def check_sensitive_paths():
|
||||
section("5. Sensitive Path Exposure")
|
||||
for path in SENSITIVE_PATHS:
|
||||
r = get(path, allow_redirects=False)
|
||||
if r is None:
|
||||
continue
|
||||
if r.status_code == 200:
|
||||
size = len(r.content)
|
||||
if path in ("/robots.txt", "/sitemap.xml"):
|
||||
info(f"{r.status_code} {path} ({size}B)")
|
||||
add("INFO", f"Found: {path}", r.text[:300])
|
||||
else:
|
||||
bad(f"{r.status_code} {path} ({size}B) ← EXPOSED")
|
||||
add("HIGH", f"Sensitive path exposed: {path}", f"HTTP 200, {size} bytes")
|
||||
elif r.status_code in (301, 302):
|
||||
loc = r.headers.get("Location", "")
|
||||
info(f"{r.status_code} {path} → {loc}")
|
||||
elif r.status_code == 403:
|
||||
warn(f"403 {path} (exists but forbidden — still interesting)")
|
||||
add("LOW", f"Forbidden path (exists): {path}")
|
||||
|
||||
|
||||
# ── 6. robots.txt & sitemap ───────────────────────────────────────────────────
|
||||
def check_robots():
|
||||
section("6. robots.txt Analysis")
|
||||
r = get("/robots.txt")
|
||||
if r and r.status_code == 200:
|
||||
info("robots.txt found:")
|
||||
disallowed = [l for l in r.text.splitlines() if "Disallow" in l]
|
||||
for line in disallowed:
|
||||
warn(f" {line.strip()} ← attacker will check these first")
|
||||
path = line.split(":", 1)[-1].strip()
|
||||
add("INFO", "robots.txt Disallow path", path)
|
||||
else:
|
||||
info("No robots.txt found")
|
||||
|
||||
|
||||
# ── 7. Open Redirect ─────────────────────────────────────────────────────────
|
||||
def check_open_redirect():
|
||||
section("7. Open Redirect Probe")
|
||||
redirect_params = ["redirect", "url", "next", "return", "returnUrl", "goto", "dest", "target"]
|
||||
for param in redirect_params:
|
||||
for payload in OPEN_REDIRECT_PAYLOADS[:1]: # Just one payload for non-destructive check
|
||||
r = get("/", params={param: payload}, allow_redirects=False)
|
||||
if r and r.status_code in (301, 302, 303, 307, 308):
|
||||
loc = r.headers.get("Location", "")
|
||||
if "evil.com" in loc:
|
||||
bad(f"Open redirect via ?{param}= → {loc}")
|
||||
add("HIGH", "Open Redirect", f"?{param}={payload} → {loc}")
|
||||
else:
|
||||
info(f"Redirect on ?{param}= but to: {loc} (not vulnerable)")
|
||||
# No redirect = not vulnerable to this param
|
||||
|
||||
|
||||
# ── 8. XSS Canary in Search ───────────────────────────────────────────────────
|
||||
def check_xss():
|
||||
section("8. Reflected XSS Canary (search param)")
|
||||
search_paths = ["/", "/repertoire", "/search"]
|
||||
search_params = ["q", "s", "query", "search", "keyword", "term"]
|
||||
|
||||
for path in search_paths:
|
||||
for param in search_params:
|
||||
r = get(path, params={param: XSS_CANARY})
|
||||
if r and XSS_CANARY in r.text:
|
||||
bad(f"XSS canary reflected unencoded at {path}?{param}=")
|
||||
add("CRITICAL", "Reflected XSS", f"{path}?{param}={XSS_CANARY}")
|
||||
elif r and urllib.parse.quote(XSS_CANARY) in r.text:
|
||||
ok(f"{path}?{param}= — canary is URL-encoded (likely safe)")
|
||||
elif r and "<script>" in r.text.lower():
|
||||
ok(f"{path}?{param}= — canary is HTML-encoded (safe)")
|
||||
|
||||
|
||||
# ── 9. Directory Listing ──────────────────────────────────────────────────────
|
||||
def check_dir_listing():
|
||||
section("9. Directory Listing")
|
||||
dirs = ["/uploads/", "/files/", "/media/", "/static/", "/assets/"]
|
||||
for path in dirs:
|
||||
r = get(path)
|
||||
if r and r.status_code == 200:
|
||||
markers = ["index of", "parent directory", "<title>directory"]
|
||||
if any(m in r.text.lower() for m in markers):
|
||||
bad(f"Directory listing ENABLED at {path}")
|
||||
add("HIGH", "Directory listing enabled", path)
|
||||
else:
|
||||
info(f"{path} → 200 but no listing detected")
|
||||
|
||||
|
||||
# ── 10. CORS Misconfiguration ─────────────────────────────────────────────────
|
||||
def check_cors():
|
||||
section("10. CORS Misconfiguration")
|
||||
try:
|
||||
r = requests.get(
|
||||
TARGET_BASE + "/api",
|
||||
headers={**HEADERS, "Origin": "https://evil.com"},
|
||||
timeout=TIMEOUT,
|
||||
verify=True,
|
||||
)
|
||||
acao = r.headers.get("Access-Control-Allow-Origin", "")
|
||||
acac = r.headers.get("Access-Control-Allow-Credentials", "")
|
||||
if acao == "*":
|
||||
warn("CORS: Access-Control-Allow-Origin: * (wildcard — no credentials risk)")
|
||||
add("LOW", "CORS wildcard origin", "ACAO: *")
|
||||
elif acao == "https://evil.com":
|
||||
bad("CORS reflects arbitrary Origin!")
|
||||
add("HIGH", "CORS reflects arbitrary Origin", f"ACAO: {acao}, ACAC: {acac}")
|
||||
if acac.lower() == "true":
|
||||
add("CRITICAL", "CORS reflects Origin + credentials allowed", "Full CORS bypass")
|
||||
else:
|
||||
ok(f"CORS origin: '{acao}' (not reflected)")
|
||||
except Exception as e:
|
||||
info(f"CORS check skipped: {e}")
|
||||
|
||||
|
||||
# ── 11. Subdomain Enumeration ─────────────────────────────────────────────────
|
||||
def check_subdomains():
|
||||
section("11. Subdomain Enumeration (passive wordlist)")
|
||||
parent = ".".join(TARGET_HOST.split(".")[-2:]) # erg.be
|
||||
found = []
|
||||
for sub in SUBDOMAIN_WORDLIST:
|
||||
fqdn = f"{sub}.{parent}"
|
||||
try:
|
||||
ip = socket.gethostbyname(fqdn)
|
||||
info(f"Found: {fqdn} → {ip}")
|
||||
found.append(fqdn)
|
||||
add("INFO", "Subdomain found", f"{fqdn} → {ip}")
|
||||
except socket.gaierror:
|
||||
pass
|
||||
if not found:
|
||||
ok("No additional subdomains discovered from wordlist")
|
||||
|
||||
|
||||
# ── 12. HSTS Preload ──────────────────────────────────────────────────────────
|
||||
def check_hsts():
|
||||
section("12. HSTS & HTTPS Redirect")
|
||||
# Check HTTP → HTTPS redirect
|
||||
try:
|
||||
r = requests.get(f"http://{TARGET_HOST}/", headers=HEADERS, timeout=TIMEOUT,
|
||||
allow_redirects=False, verify=False)
|
||||
if r.status_code in (301, 302):
|
||||
loc = r.headers.get("Location", "")
|
||||
if loc.startswith("https://"):
|
||||
ok(f"HTTP redirects to HTTPS ({r.status_code})")
|
||||
else:
|
||||
bad(f"HTTP redirects to non-HTTPS: {loc}")
|
||||
add("HIGH", "HTTP does not redirect to HTTPS", loc)
|
||||
else:
|
||||
bad(f"HTTP returns {r.status_code} (no HTTPS redirect)")
|
||||
add("HIGH", "No HTTP→HTTPS redirect", f"Status: {r.status_code}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
r = get("/")
|
||||
if r:
|
||||
hsts = r.headers.get("Strict-Transport-Security", "")
|
||||
if "max-age" in hsts:
|
||||
age = re.search(r"max-age=(\d+)", hsts)
|
||||
age_val = int(age.group(1)) if age else 0
|
||||
if age_val < 31536000:
|
||||
warn(f"HSTS max-age too short: {age_val}s (recommend ≥31536000)")
|
||||
add("LOW", "HSTS max-age too short", f"{age_val}s")
|
||||
else:
|
||||
ok(f"HSTS max-age OK: {age_val}s")
|
||||
if "includeSubDomains" not in hsts:
|
||||
warn("HSTS missing includeSubDomains")
|
||||
if "preload" not in hsts:
|
||||
warn("HSTS missing preload directive")
|
||||
else:
|
||||
bad("HSTS header missing entirely")
|
||||
add("HIGH", "HSTS not set")
|
||||
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
def print_summary():
|
||||
section("SUMMARY REPORT")
|
||||
order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4}
|
||||
color_map = {
|
||||
"CRITICAL": Fore.RED,
|
||||
"HIGH": Fore.YELLOW,
|
||||
"MEDIUM": Fore.CYAN,
|
||||
"LOW": Fore.WHITE,
|
||||
"INFO": Fore.BLUE,
|
||||
}
|
||||
sorted_findings = sorted(findings, key=lambda f: order.get(f.severity, 9))
|
||||
counts = {k: 0 for k in order}
|
||||
for f in sorted_findings:
|
||||
counts[f.severity] = counts.get(f.severity, 0) + 1
|
||||
c = color_map.get(f.severity, "")
|
||||
print(f" {c}[{f.severity:8s}]{Style.RESET_ALL} {f.title}")
|
||||
if f.detail:
|
||||
detail_preview = f.detail[:120].replace("\n", " ")
|
||||
print(f" └─ {detail_preview}")
|
||||
|
||||
print()
|
||||
for sev, cnt in counts.items():
|
||||
if cnt:
|
||||
c = color_map[sev]
|
||||
print(f" {c}{sev}: {cnt}{Style.RESET_ALL}")
|
||||
|
||||
print(f"\n Scan completed: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f" Target: {TARGET_BASE}")
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
if __name__ == "__main__":
|
||||
print(f"""
|
||||
{Fore.CYAN}╔══════════════════════════════════════════════════════════╗
|
||||
║ Passive Pentest Scanner — authorized use only ║
|
||||
║ Target : {TARGET_HOST:<44} ║
|
||||
╚══════════════════════════════════════════════════════════╝{Style.RESET_ALL}
|
||||
""")
|
||||
check_dns()
|
||||
check_tls()
|
||||
check_headers()
|
||||
check_cookies()
|
||||
check_sensitive_paths()
|
||||
check_robots()
|
||||
check_open_redirect()
|
||||
check_xss()
|
||||
check_dir_listing()
|
||||
check_cors()
|
||||
check_subdomains()
|
||||
check_hsts()
|
||||
print_summary()
|
||||
Reference in New Issue
Block a user