diff --git a/monitoring/security/wazuh/config/wazuh_manager/ossec.conf b/monitoring/security/wazuh/config/wazuh_manager/ossec.conf
index e8611ad3..290dec99 100755
--- a/monitoring/security/wazuh/config/wazuh_manager/ossec.conf
+++ b/monitoring/security/wazuh/config/wazuh_manager/ossec.conf
@@ -17,7 +17,7 @@
ALERTES
═══════════════════════════════════════════════════════════ -->
- 4
+ 3
127.0.0.1
^localhost.localdomain$
90.50.148.138
+ 90.120.69.13
10.0.0.3
diff --git a/monitoring/security/wazuh/create-saved-queries-web.py b/monitoring/security/wazuh/create-saved-queries-web.py
new file mode 100644
index 00000000..9dc7daf1
--- /dev/null
+++ b/monitoring/security/wazuh/create-saved-queries-web.py
@@ -0,0 +1,107 @@
+#!/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()
diff --git a/monitoring/security/wazuh/create-saved-query-no-vpn.py b/monitoring/security/wazuh/create-saved-query-no-vpn.py
new file mode 100644
index 00000000..f813028b
--- /dev/null
+++ b/monitoring/security/wazuh/create-saved-query-no-vpn.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+"""
+Crée une Saved Query "Sans VPN" réutilisable dans OpenSearch Dashboards (Wazuh).
+
+Objectif : permettre d'exclure en un clic les alertes WireGuard/VPN (handshakes,
+timeouts, roaming, reconnexions — rules 100800-100826) de la vue native
+"Security Events" / "Threat Hunting", qui liste par défaut TOUTES les alertes
+sans filtre de groupe. Le dashboard dédié "VPN - Activite WireGuard"
+(create-dashboard-vpn.py) continue lui d'afficher ces événements normalement :
+cette query ne supprime rien de l'index, elle filtre uniquement l'affichage.
+
+Usage:
+ docker cp create-saved-query-no-vpn.py wazuh_dashboard:/tmp/
+ docker exec wazuh_dashboard python3 /tmp/create-saved-query-no-vpn.py
+
+Une fois créée, ouvrir "Security Events" > barre de recherche > icône
+"Saved Queries" (dossier) > sélectionner "Sans VPN".
+"""
+
+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!")
+
+QUERY_ID = "no-vpn-alerts"
+QUERY_TITLE = "Sans VPN"
+QUERY_DESC = "Exclut les alertes WireGuard/VPN (handshakes, timeouts, roaming) de la vue Security Events."
+QUERY_KUERY = "NOT rule.groups: wireguard"
+
+# ── 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 Query: Sans VPN ===\n")
+ body = {
+ "attributes": {
+ "title": QUERY_TITLE,
+ "description": QUERY_DESC,
+ "query": {"language": "kuery", "query": QUERY_KUERY},
+ "filters": [],
+ }
+ }
+ r = api("POST", f"/api/saved_objects/query/{QUERY_ID}?overwrite=true", body)
+ if "id" in r:
+ print(f" OK Saved Query '{QUERY_TITLE}' créée ({r['id']})")
+ print(" Utilisation : Security Events > barre de recherche > icone dossier (Saved Queries) > 'Sans VPN'")
+ else:
+ print(f" ERR {r}")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()