#!/usr/bin/env python3 """ Crée des Saved Queries réutilisables pour la détection d'attaques web (ModSecurity/OWASP, rules 100100-100396 — voir config/wazuh_manager/rules/modsecurity_owasp_rules.xml) dans OpenSearch Dashboards (Wazuh). Usage: docker cp create-saved-queries-web.py wazuh_dashboard:/tmp/ docker exec wazuh_dashboard python3 /tmp/create-saved-queries-web.py Une fois créées, ouvrir n'importe quelle vue (Security Events, Discover...) > barre de recherche > icone dossier (Saved Queries) > sélectionner la query. """ import json import urllib.request import urllib.error import ssl import base64 import sys import os # ── Configuration ──────────────────────────────────────────────────────────── DASHBOARD_HOST = "https://localhost:5601" DASHBOARD_USER = "kibanaserver" DASHBOARD_PASS = os.environ.get("DASHBOARD_PASSWORD", "E9Jpr6586kQ3wYrCS2!") # ── Queries ─────────────────────────────────────────────────────────────────── QUERIES = [ { "id": "web-attacks-all", "title": "Attaques Web - Toutes", "description": "Tous les événements ModSecurity/OWASP (bloqués + remontés en anomaly scoring).", "kuery": "rule.groups: web", }, { "id": "web-attacks-blocked", "title": "Attaques Web - Bloquées (WAF)", "description": "Requêtes réellement bloquées par le WAF (exclut les anomaly scoring non bloquants, groupe 'warning').", "kuery": "rule.groups: web AND NOT rule.groups: warning", }, { "id": "web-attacks-critical", "title": "Attaques Web - Critiques", "description": "Niveau >= 13 : SQLi/RCE/command injection bloquées, XXE, SSRF, patterns répétés (owasp_injection critical/brute_force).", "kuery": "rule.groups: web AND rule.level >= 13", }, { "id": "web-attacks-scanners", "title": "Attaques Web - Scanners / Recon", "description": "Détection de scanners/bots (OWASP#9 — groupe scanner_detection).", "kuery": "rule.groups: scanner_detection", }, { "id": "web-attacks-repeated", "title": "Attaques Web - Patterns répétés (brute-force)", "description": "Attaques soutenues détectées par fréquence : SQLi (100290), XSS (100291), RCE (100292), path traversal (100293), anomaly scoring répété (100394-100396).", "kuery": "rule.id: 100290 OR rule.id: 100291 OR rule.id: 100292 OR rule.id: 100293 OR rule.id: 100394 OR rule.id: 100395 OR rule.id: 100396", }, ] # ── Client HTTP ─────────────────────────────────────────────────────────────── ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE _auth = base64.b64encode(f"{DASHBOARD_USER}:{DASHBOARD_PASS}".encode()).decode() _headers = { "Content-Type": "application/json", "osd-xsrf": "true", "Authorization": f"Basic {_auth}", } def api(method, path, body=None): data = json.dumps(body).encode() if body else None req = urllib.request.Request(DASHBOARD_HOST + path, data=data, headers=_headers, method=method) try: with urllib.request.urlopen(req, context=ctx, timeout=15) as r: return json.loads(r.read()) except urllib.error.HTTPError as e: return {"error": e.code, "msg": e.read().decode()[:300]} def main(): print("=== Creation Saved Queries: Attaques Web ===\n") failed = False for q in QUERIES: body = { "attributes": { "title": q["title"], "description": q["description"], "query": {"language": "kuery", "query": q["kuery"]}, "filters": [], } } r = api("POST", f"/api/saved_objects/query/{q['id']}?overwrite=true", body) if "id" in r: print(f" OK {q['title']}") else: print(f" ERR {q['title']}: {r}") failed = True if failed: sys.exit(1) print("\n=== Termine ===") if __name__ == "__main__": main()