#!/usr/bin/env python3 """ Test automatisé de sécurité et UX — laurentkeller.org Tourne en cron, archive les résultats, détecte les régressions. """ import json import subprocess import sys import os from datetime import datetime, timezone from pathlib import Path # Configuration SITE = "https://laurentkeller.org" REPORT_DIR = Path(os.path.expanduser("~/.hermes/cron/output/site-tests")) REPORT_DIR.mkdir(parents=True, exist_ok=True) HISTORY_FILE = REPORT_DIR / "history.json" def curl(url, **kwargs): """Wrapper curl avec timeout.""" cmd = ["curl", "-s", "--connect-timeout", "5", "--max-time", "10"] for k, v in kwargs.items(): if v is True: cmd.append(f"--{k.replace('_', '-')}") elif v: cmd.append(f"--{k.replace('_', '-')}") cmd.append(str(v)) cmd.append(url) try: r = subprocess.run(cmd, capture_output=True, text=True, timeout=15) return {"exit": r.returncode, "stdout": r.stdout, "stderr": r.stderr} except subprocess.TimeoutExpired: return {"exit": -1, "stdout": "", "stderr": "timeout"} def test_headers(): """Teste les en-têtes de sécurité.""" r = curl(SITE, head=True) headers_raw = r["stdout"].lower() results = { "strict-transport-security": "max-age=31536000; includesubdomains" in headers_raw, "x-frame-options": "deny" in headers_raw, "x-content-type-options": "nosniff" in headers_raw, "content-security-policy": "default-src 'self'" in headers_raw, "upgrade-insecure-requests": "upgrade-insecure-requests" in headers_raw, "permissions-policy": "camera=(), microphone=(), geolocation=()" in headers_raw, "referrer-policy": "strict-origin-when-cross-origin" in headers_raw, "x-powered-by": "next.js" in headers_raw, } # Extraire le CSP complet csp_line = [l for l in r["stdout"].split("\n") if "content-security-policy:" in l.lower()] results["csp_img_src"] = "s3.keller-laurent.org" in (csp_line[0] if csp_line else "") return results def test_http_redirect(): """Teste la redirection HTTP → HTTPS.""" r = curl("http://laurentkeller.org/", head=True, location=False) has_redirect = "301" in r["stderr"] or "location: https://" in r["stdout"].lower() return has_redirect def test_tls(): """Teste la version TLS.""" r = subprocess.run( ["openssl", "s_client", "-servername", "laurentkeller.org", "-connect", "laurentkeller.org:443", "-tlsextdebug"], capture_output=True, text=True, input="", timeout=10 ) tls_version = "unknown" for line in r.stdout.split("\n"): if "Protocol" in line: tls_version = line.strip() break return tls_version def test_sensitive_files(): """Teste l'accès aux fichiers sensibles.""" paths = [ "/.env", "/.env.local", "/package.json", "/.git/config", "/.git/HEAD", "/admin", "/api/auth-debug", "/config.json", "/backup", "/wp-admin", ] results = {} for path in paths: r = curl(f"{SITE}{path}", head=True) code = r["stdout"].split("\n")[0] if r["stdout"] else "000" # Extraire le code HTTP if "HTTP/" in code: code = code.split()[1] elif r["exit"] != 0: code = "000" else: code = "200" # fallback results[path] = code return results def test_methods(): """Teste les méthodes HTTP non autorisées.""" methods = ["PUT", "DELETE", "PATCH", "OPTIONS", "TRACE"] results = {} for method in methods: r = subprocess.run( ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-X", method, "--connect-timeout", "5", "--max-time", "10", f"{SITE}/galerie"], capture_output=True, text=True, timeout=12 ) results[method] = r.stdout.strip() return results def test_xss(): """Teste XSS basique.""" r = curl(f"{SITE}/galerie?q=") blocked = "