feat: add passive pentest scanner script with PEP 723 uv metadata

This commit is contained in:
Pontoporeia
2026-05-11 17:56:30 +02:00
parent df12af8423
commit 4717b4d67e
2 changed files with 482 additions and 0 deletions

View File

@@ -1429,6 +1429,19 @@
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +%%%%%%% 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) +\\\\\\\ to: osltsstu c8c1516d "fix: validation error messages hidden by generic fallback in ErrorHandler::userMessage" (rebased revision)
++ $linkName = $link['name'] ?? ''; ++ $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'])) : ''; ++ $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"> <tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">

469
pentest_xamxam.py Normal file
View 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 "&lt;script&gt;" 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()