diff --git a/monitoring/security/docker-compose-security.yml b/monitoring/security/docker-compose-security.yml
index 7fdd3cd7..8daa321c 100755
--- a/monitoring/security/docker-compose-security.yml
+++ b/monitoring/security/docker-compose-security.yml
@@ -105,6 +105,8 @@ services:
- crowdsec_logs:/var/log/crowdsec:ro
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/local/bin/docker:ro
+ - ./wazuh/config/wazuh_manager/active-response:/wazuh-custom/active-response:ro
+ - ./wazuh/config/wazuh_manager/agent.conf:/wazuh-custom/agent.conf:ro
networks:
- monitoring_net
diff --git a/monitoring/security/wazuh/config/wazuh_manager/active-response/crowdsec-ban.sh b/monitoring/security/wazuh/config/wazuh_manager/active-response/crowdsec-ban.sh
new file mode 100755
index 00000000..cedb2c99
--- /dev/null
+++ b/monitoring/security/wazuh/config/wazuh_manager/active-response/crowdsec-ban.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+# Wazuh Active Response -> CrowdSec ban
+
+read INPUT
+
+SRC_IP=$(echo "$INPUT" | python3 -c "
+import sys, json, re
+try:
+ d = json.load(sys.stdin)
+ alert = d.get('parameters', {}).get('alert', {})
+ data = alert.get('data', {})
+ # 1. Try standard srcip field
+ ip = data.get('srcip') or data.get('src_ip') or ''
+ # 2. Fallback: extract first public IP from full_log
+ if not ip:
+ full_log = alert.get('full_log', '')
+ ips = re.findall(r'\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b', full_log)
+ private = re.compile(r'^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.)')
+ for candidate in ips:
+ if not private.match(candidate):
+ ip = candidate
+ break
+ print(ip)
+except:
+ print('')
+" 2>/dev/null)
+
+ACTION=$(echo "$INPUT" | python3 -c "
+import sys, json
+try:
+ d = json.load(sys.stdin)
+ print(d.get('command', 'add'))
+except:
+ print('add')
+" 2>/dev/null)
+
+LOG=/var/ossec/logs/active-responses.log
+
+if [ -z "$SRC_IP" ] || [ "$SRC_IP" = "null" ]; then
+ echo "$(date) crowdsec-ban: no IP found" >> "$LOG"
+ exit 0
+fi
+
+# Ne pas bannir les IPs privees / VPN
+if echo "$SRC_IP" | grep -qE '^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[01])\.|^127\.'; then
+ echo "$(date) crowdsec-ban: skip private IP $SRC_IP" >> "$LOG"
+ exit 0
+fi
+
+if [ "$ACTION" = "delete" ]; then
+ /usr/local/bin/docker exec crowdsec cscli decisions delete --ip "$SRC_IP" 2>/dev/null
+ echo "$(date) crowdsec-ban: UNBAN $SRC_IP" >> "$LOG"
+else
+ /usr/local/bin/docker exec crowdsec cscli decisions add --ip "$SRC_IP" \
+ --duration 24h --reason Wazuh-ActiveResponse --type ban 2>&1 >> "$LOG"
+ echo "$(date) crowdsec-ban: BAN $SRC_IP" >> "$LOG"
+fi
diff --git a/monitoring/security/wazuh/config/wazuh_manager/agent.conf b/monitoring/security/wazuh/config/wazuh_manager/agent.conf
new file mode 100644
index 00000000..9e7bc5d2
--- /dev/null
+++ b/monitoring/security/wazuh/config/wazuh_manager/agent.conf
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+ apache
+ /var/log/nginx/access.log
+
+
+
+ syslog
+ /var/log/nginx/error.log
+
+
+
+
+
+ syslog
+ /var/lib/docker/containers/*/*-json.log
+
+
+
+
+
+ syslog
+ /var/log/auth.log
+
+
+
+
+ syslog
+ /var/log/syslog
+
+
+
+
diff --git a/monitoring/security/wazuh/config/wazuh_manager/decoders/api-decoder.xml b/monitoring/security/wazuh/config/wazuh_manager/decoders/api-decoder.xml
new file mode 100644
index 00000000..32d7a8e1
--- /dev/null
+++ b/monitoring/security/wazuh/config/wazuh_manager/decoders/api-decoder.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ backend
+
+
+
+
+ go-backend
+ [GIN]
+ \|\s+(\d{3})\s+\|\s+(\S+)\s+\|\s+([\d.]+)\s+\|\s+(\S+)\s+(\S+)
+ id,extra_data,srcip,protocol,url
+
+
+
+
+ go-backend
+ LOGIN_ADMIN|LOGIN_CLIENT
+ LOGIN_\S+ \S+ \S+ \S+: (\S+)
+ srcuser
+
+
+
+
+ LOGIN_ADMIN|LOGIN_CLIENT
+ LOGIN_\S+ \S+ \S+ \S+: (\S+)
+ srcuser
+
diff --git a/monitoring/security/wazuh/config/wazuh_manager/decoders/crowdsec-decoder.xml b/monitoring/security/wazuh/config/wazuh_manager/decoders/crowdsec-decoder.xml
index a3236de3..8f3d7efa 100644
--- a/monitoring/security/wazuh/config/wazuh_manager/decoders/crowdsec-decoder.xml
+++ b/monitoring/security/wazuh/config/wazuh_manager/decoders/crowdsec-decoder.xml
@@ -1,24 +1,38 @@
-
+
- ^time="20\d\d-\d\d-\d\dT\d\d:\d\d:\d\d
-
-
-
- crowdsec
- level=(\S+) msg="(\.+)"
- status,extra_data
+ ^time="\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}
+
crowdsec
- ban Ip |ban ip |type":"ban
- (\d+\.\d+\.\d+\.\d+)
+ ban on Ip
+ ban on Ip ([\d.]+)
srcip
-
+
+
crowdsec
- overflow from |triggered |New overflow
- '(\d+\.\d+\.\d+\.\d+)'
+ ban on ip
+ ban on ip ([\d.]+)
srcip
+
+
+
+ crowdsec
+ overflow from
+ overflow from ([\d.]+)
+ srcip
+
+
+
+
+ crowdsec
+ level=(\S+) msg="(.+?)"
+ status,extra_data
+
diff --git a/monitoring/security/wazuh/config/wazuh_manager/decoders/wireguard-decoder.xml b/monitoring/security/wazuh/config/wazuh_manager/decoders/wireguard-decoder.xml
new file mode 100644
index 00000000..e07ce144
--- /dev/null
+++ b/monitoring/security/wazuh/config/wazuh_manager/decoders/wireguard-decoder.xml
@@ -0,0 +1,56 @@
+
+
+
+
+ kernel
+ wireguard: wg0:
+
+
+
+
+ wireguard
+ Sending handshake initiation
+ wireguard: wg0: Sending handshake initiation to peer (\d+) \(([\d.]+):\d+\)
+ id,srcip
+
+
+
+
+ wireguard
+ Sending handshake response
+ wireguard: wg0: Sending handshake response to peer (\d+) \(([\d.]+):\d+\)
+ id,srcip
+
+
+
+
+ wireguard
+ Receiving handshake response
+ wireguard: wg0: Receiving handshake response from peer (\d+) \(([\d.]+):\d+\)
+ id,srcip
+
+
+
+
+ wireguard
+ keepalive packet
+ wireguard: wg0: \S+ keepalive packet \S+ peer (\d+) \(([\d.]+):\d+\)
+ id,srcip
+
+
+
+
+ wireguard
+ did not complete
+ wireguard: wg0: Handshake for peer (\d+)[^(]+\(([\d.]+):\d+\).+retrying \(try (\d+)\)
+ id,srcip,extra_data
+
+
+
+
+ wireguard
+ is now roaming
+ wireguard: wg0: Peer (\d+).+roaming to ([\d.]+):\d+
+ id,srcip
+
diff --git a/monitoring/security/wazuh/config/wazuh_manager/init.sh b/monitoring/security/wazuh/config/wazuh_manager/init.sh
index 25ecb054..bdd686a5 100755
--- a/monitoring/security/wazuh/config/wazuh_manager/init.sh
+++ b/monitoring/security/wazuh/config/wazuh_manager/init.sh
@@ -10,5 +10,23 @@ mkdir -p /var/ossec/etc/decoders /var/ossec/etc/rules
cp "$CUSTOM/decoders/"*.xml /var/ossec/etc/decoders/ 2>/dev/null || true
cp "$CUSTOM/rules/"*.xml /var/ossec/etc/rules/ 2>/dev/null || true
+# ─── Agent config partagée (distribuée aux agents) ───────────────────────────
+if [ -f "$CUSTOM/agent.conf" ]; then
+ mkdir -p /var/ossec/etc/shared/default
+ cp "$CUSTOM/agent.conf" /var/ossec/etc/shared/default/agent.conf
+fi
+
+# ─── Active-response scripts ──────────────────────────────────────────────────
+if [ -d "$CUSTOM/active-response" ]; then
+ for script in "$CUSTOM/active-response/"*.sh; do
+ [ -f "$script" ] || continue
+ dest="/var/ossec/active-response/bin/$(basename "$script")"
+ cp "$script" "$dest"
+ chmod 750 "$dest"
+ chown root:wazuh "$dest" 2>/dev/null || true
+ echo "Active-response déployé: $dest"
+ done
+fi
+
# ─── Démarrage Wazuh Manager (entrypoint original) ────────────────────────────
exec /init
diff --git a/monitoring/security/wazuh/config/wazuh_manager/ossec.conf b/monitoring/security/wazuh/config/wazuh_manager/ossec.conf
index 49fb55d2..03c48b19 100755
--- a/monitoring/security/wazuh/config/wazuh_manager/ossec.conf
+++ b/monitoring/security/wazuh/config/wazuh_manager/ossec.conf
@@ -773,11 +773,67 @@
crowdsec-ban
local
- 5712,5720,5763
+ 5710,5711,5712,5716,5720,5760,5763
86400
-
+
+
+ crowdsec-ban
+ local
+ 100300,100301
+ 86400
+
+
+
+
+ crowdsec-ban
+ local
+ 100220,100290
+ 86400
+
+
+
+
+ crowdsec-ban
+ local
+ 100222,100292
+ 0
+
+
+
+
+ crowdsec-ban
+ local
+ 100230
+ 86400
+
+
+
+
+ crowdsec-ban
+ local
+ 100240,100241,100291
+ 43200
+
+
+
+
+ crowdsec-ban
+ local
+ 100250,100251,100252,100293
+ 43200
+
+
+
+
+ crowdsec-ban
+ local
+ 100260
+ 43200
+
+
+
crowdsec-ban
local
@@ -785,7 +841,23 @@
43200
-
+
+
+ crowdsec-ban
+ local
+ 100202,100203,100204,100205,100206,100210
+ 86400
+
+
+
+
+ crowdsec-ban
+ local
+ 100610,100611,100612,100613
+ 86400
+
+
+
crowdsec-ban
local
@@ -939,6 +1011,10 @@
etc/lists/malicious-ioc/malicious-ip
etc/lists/malicious-ioc/malicious-domains
etc/lists/malicious-ioc/malware-hashes
+
+ 0565-aws-eks-authenticator_decoders.xml
etc/decoders
etc/rules
diff --git a/monitoring/security/wazuh/config/wazuh_manager/rules/api-brute-force-rules.xml b/monitoring/security/wazuh/config/wazuh_manager/rules/api-brute-force-rules.xml
new file mode 100644
index 00000000..25c5b2ca
--- /dev/null
+++ b/monitoring/security/wazuh/config/wazuh_manager/rules/api-brute-force-rules.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+ go-backend
+ Go Backend API: log entry
+ api_backend,
+
+
+
+
+ 100700
+ [GIN]
+ API: Requete HTTP Gin loggee
+ api_backend,api_http,
+
+
+
+
+ 100701
+ | 4\d\d |
+ API: Erreur client 4xx — $(srcip)
+ api_backend,api_http,api_error,
+
+
+
+
+ 100701
+ | 5\d\d |
+ API: Erreur serveur 5xx — $(srcip)
+ api_backend,api_http,api_error,
+
+
+
+
+ 100700
+ panic|PANIC|fatal|FATAL
+ API: Erreur critique Go backend (panic/fatal)
+ api_backend,api_error,
+
+
+
+
+
+
+
+
+
+ go-backend-login-fail
+ LOGIN_ADMIN
+ API: Echec auth admin
+ api_auth_failure,
+
+
+
+
+ go-backend-login-fail
+ LOGIN_CLIENT
+ API: Echec auth client
+ api_auth_failure,
+
+
+
+
+ 100600
+
+ API: Brute-force login admin — 5 echecs/60s depuis $(srcip)
+ api_brute_force,authentication_failures,
+
+
+
+
+ 100601
+
+ API: Brute-force login client — 5 echecs/60s depuis $(srcip)
+ api_brute_force,authentication_failures,
+
+
+
+
+ api_auth_failure
+
+ API: Scan auth — 20 echecs/120s depuis $(srcip)
+ api_brute_force,authentication_failures,
+
+
+
diff --git a/monitoring/security/wazuh/config/wazuh_manager/rules/wireguard-rules.xml b/monitoring/security/wazuh/config/wazuh_manager/rules/wireguard-rules.xml
new file mode 100644
index 00000000..0114b9cc
--- /dev/null
+++ b/monitoring/security/wazuh/config/wazuh_manager/rules/wireguard-rules.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+ wireguard-keepalive
+ WireGuard: keepalive peer $(srcip)
+ wireguard,vpn_keepalive,
+
+
+
+
+ wireguard-handshake-init
+ WireGuard: handshake initiation vers peer $(srcip)
+ wireguard,vpn_handshake,
+
+
+
+
+ wireguard-handshake-resp-sent
+ WireGuard: handshake reponse envoyee a peer $(srcip)
+ wireguard,vpn_handshake,
+
+
+
+
+ wireguard-handshake-resp-recv
+ WireGuard: handshake complete avec peer $(srcip)
+ wireguard,vpn_handshake,
+
+
+
+
+ wireguard-timeout
+ WireGuard: handshake timeout peer $(srcip) (essai $(extra_data))
+ wireguard,vpn_warning,
+
+
+
+
+ 100810
+ try 5|try 6|try 7|try 8|try 9
+ WireGuard: peer $(srcip) deconnecte — handshake echoue 5 fois consecutives
+ wireguard,vpn_disconnected,
+
+
+
+
+ 100810
+ retrying \(try 1\d\)
+ WireGuard: peer $(srcip) hors ligne depuis plus de 60 secondes
+ wireguard,vpn_disconnected,
+
+
+
+
+ wireguard-roaming
+ WireGuard: peer $(id) a change d'IP (roaming) vers $(srcip)
+ wireguard,vpn_roaming,
+
+
+
+
+ wireguard-handshake-resp-recv
+ WireGuard: peer $(srcip) handshake complet (reconnexion possible)
+ wireguard,vpn_reconnected,
+
+
+
diff --git a/monitoring/security/wazuh/create-dashboard-api.py b/monitoring/security/wazuh/create-dashboard-api.py
new file mode 100644
index 00000000..abc76192
--- /dev/null
+++ b/monitoring/security/wazuh/create-dashboard-api.py
@@ -0,0 +1,333 @@
+#!/usr/bin/env python3
+"""
+Crée le dashboard "Securite - API Backend & WAF" dans OpenSearch Dashboards (Wazuh).
+
+Usage:
+ python3 create-dashboard-api.py
+
+Le script se connecte depuis l'intérieur du container wazuh_dashboard via:
+ docker exec wazuh_dashboard python3 /tmp/create-dashboard-api.py
+
+Données utilisées :
+ - rules 100101 (WAF alerté) / 100102 (WAF bloqué) → ModSecurity
+ - rules 100600-100613 → Brute-force login API
+ - fields: data.transaction.client_ip, .request.uri,
+ .response.http_code, .is_interrupted,
+ .messages.details.ruleId
+"""
+
+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!")
+INDEX_PATTERN = "wazuh-alerts-*"
+
+# ── Queries ───────────────────────────────────────────────────────────────────
+Q_WAF = "rule.id: 100101 OR rule.id: 100102"
+Q_BLOCKED = "rule.id: 100102"
+Q_ALERTED = "rule.id: 100101"
+Q_BFORCE = "rule.groups: api_brute_force"
+Q_ALL = Q_WAF + " OR " + Q_BFORCE
+
+# ── 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 search_src(query):
+ return json.dumps({
+ "index": INDEX_PATTERN,
+ "filter": [],
+ "query": {"language": "kuery", "query": query},
+ })
+
+# ── Visualisations ────────────────────────────────────────────────────────────
+VISUALIZATIONS = [
+ # ── 1. Métrique : total alertes WAF ──────────────────────────────────────
+ {
+ "id": "api-waf-total",
+ "title": "[API] WAF - Total Alertes",
+ "type": "metric",
+ "query": Q_WAF,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False,
+ "useRanges": False,
+ "colorSchema": "Green to Red",
+ "metricColorMode": "Labels",
+ "colorsRange": [{"from": 0, "to": 50}, {"from": 50, "to": 500}, {"from": 500, "to": 99999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False, "subText": "alertes WAF", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ # ── 2. Métrique : requêtes bloquées ──────────────────────────────────────
+ {
+ "id": "api-waf-blocked",
+ "title": "[API] WAF - Requetes Bloquees",
+ "type": "metric",
+ "query": Q_BLOCKED,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False,
+ "useRanges": False,
+ "colorSchema": "Green to Red",
+ "metricColorMode": "Labels",
+ "colorsRange": [{"from": 0, "to": 10}, {"from": 10, "to": 100}, {"from": 100, "to": 99999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False, "subText": "bloquees par WAF", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ # ── 3. Métrique : brute-force login ──────────────────────────────────────
+ {
+ "id": "api-bf-count",
+ "title": "[API] Brute-Force Login",
+ "type": "metric",
+ "query": Q_BFORCE,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False,
+ "useRanges": False,
+ "colorSchema": "Green to Red",
+ "metricColorMode": "Labels",
+ "colorsRange": [{"from": 0, "to": 5}, {"from": 5, "to": 50}, {"from": 50, "to": 99999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False, "subText": "alertes brute-force", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ # ── 4. Timeline : activite WAF ────────────────────────────────────────────
+ {
+ "id": "api-waf-timeline",
+ "title": "[API] Timeline Activite WAF",
+ "type": "histogram",
+ "query": Q_ALL,
+ "visState": {
+ "type": "histogram",
+ "params": {
+ "type": "histogram",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "bottom", "show": True, "style": {}, "scale": {"type": "linear"}, "labels": {"show": True, "truncate": 100}, "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "left", "show": True, "style": {}, "scale": {"type": "linear", "mode": "normal"}, "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100}, "title": {"text": "Requetes"}}],
+ "seriesParams": [{"show": True, "type": "histogram", "mode": "stacked", "data": {"label": "Requetes", "id": "1"}, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": True, "showCircles": True}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right", "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "date_histogram", "schema": "segment", "params": {"field": "@timestamp", "interval": "auto", "customInterval": "2h", "min_doc_count": 1, "extended_bounds": {}}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "group", "params": {"field": "rule.id", "size": 5, "order": "desc", "orderBy": "1", "otherBucket": False}},
+ ],
+ },
+ },
+ # ── 5. Bar horizontal : top endpoints ciblés ─────────────────────────────
+ {
+ "id": "api-top-endpoints",
+ "title": "[API] Top Endpoints Cibles",
+ "type": "horizontal_bar",
+ "query": Q_WAF,
+ "visState": {
+ "type": "horizontal_bar",
+ "params": {
+ "type": "horizontal_bar",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "left", "show": True, "style": {}, "scale": {"type": "linear"}, "labels": {"show": True, "rotate": 0, "filter": True, "truncate": 250}, "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "bottom", "show": True, "style": {}, "scale": {"type": "linear", "mode": "normal"}, "labels": {"show": True, "rotate": 75, "filter": False, "truncate": 100}, "title": {"text": "Requetes"}}],
+ "seriesParams": [{"show": True, "type": "horizontal_bar", "mode": "normal", "data": {"label": "Requetes", "id": "1"}, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": True, "showCircles": True}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right", "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "data.transaction.request.uri", "size": 15, "order": "desc", "orderBy": "1", "otherBucket": True, "otherBucketLabel": "Autres", "missingBucket": False}},
+ ],
+ },
+ },
+ # ── 6. Bar horizontal : top IPs attaquantes ───────────────────────────────
+ {
+ "id": "api-top-ips",
+ "title": "[API] Top IPs Attaquantes",
+ "type": "horizontal_bar",
+ "query": Q_ALL,
+ "visState": {
+ "type": "horizontal_bar",
+ "params": {
+ "type": "horizontal_bar",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "left", "show": True, "style": {}, "scale": {"type": "linear"}, "labels": {"show": True, "rotate": 0, "filter": True, "truncate": 200}, "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "bottom", "show": True, "style": {}, "scale": {"type": "linear", "mode": "normal"}, "labels": {"show": True, "rotate": 75, "filter": False, "truncate": 100}, "title": {"text": "Alertes"}}],
+ "seriesParams": [{"show": True, "type": "horizontal_bar", "mode": "normal", "data": {"label": "Alertes", "id": "1"}, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": True, "showCircles": True}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right", "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "data.transaction.client_ip", "size": 15, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
+ ],
+ },
+ },
+ # ── 7. Pie : distribution codes HTTP ─────────────────────────────────────
+ {
+ "id": "api-http-codes",
+ "title": "[API] Distribution Codes HTTP",
+ "type": "pie",
+ "query": Q_WAF,
+ "visState": {
+ "type": "pie",
+ "params": {
+ "type": "pie", "addTooltip": True, "addLegend": True,
+ "legendPosition": "right", "isDonut": True,
+ "labels": {"show": True, "values": True, "last_level": True, "truncate": 100},
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "data.transaction.response.http_code", "size": 10, "order": "desc", "orderBy": "1", "otherBucket": True, "otherBucketLabel": "Autres", "missingBucket": False}},
+ ],
+ },
+ },
+ # ── 8. Pie : bloqué vs passé ──────────────────────────────────────────────
+ {
+ "id": "api-blocked-ratio",
+ "title": "[API] Bloque vs Alerte WAF",
+ "type": "pie",
+ "query": Q_WAF,
+ "visState": {
+ "type": "pie",
+ "params": {
+ "type": "pie", "addTooltip": True, "addLegend": True,
+ "legendPosition": "right", "isDonut": False,
+ "labels": {"show": True, "values": True, "last_level": True, "truncate": 100},
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "data.transaction.is_interrupted", "size": 5, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
+ ],
+ },
+ },
+ # ── 9. Table : top règles CRS déclenchées ────────────────────────────────
+ {
+ "id": "api-crs-rules",
+ "title": "[API] Top Regles CRS Declenchees",
+ "type": "table",
+ "query": Q_WAF,
+ "visState": {
+ "type": "table",
+ "params": {
+ "perPage": 15, "showPartialRows": False, "showMetricsAtAllLevels": False,
+ "sort": {"columnIndex": None, "direction": None}, "showTotal": False, "totalFunc": "sum",
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "bucket", "params": {"field": "data.transaction.messages.details.ruleId", "size": 15, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "bucket", "params": {"field": "data.transaction.messages.message", "size": 1, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
+ ],
+ },
+ "uiStateJSON": json.dumps({"vis": {"params": {"sort": {"columnIndex": 0, "direction": "desc"}}}}),
+ },
+]
+
+# ── Layout dashboard (grille 48 colonnes) ─────────────────────────────────────
+# Ligne 0 : 3 métriques (total | bloquées | brute-force)
+# Ligne 6 : timeline pleine largeur
+# Ligne 18: top endpoints (gauche) + top IPs (droite)
+# Ligne 32: codes HTTP (gauche) + bloqué/passé (milieu) + règles CRS (droite)
+PANELS = [
+ # Métriques
+ {"panelIndex": "1", "gridData": {"x": 0, "y": 0, "w": 16, "h": 6, "i": "1"}, "version": "2.19.5", "type": "visualization", "id": "api-waf-total", "embeddableConfig": {}},
+ {"panelIndex": "2", "gridData": {"x": 16, "y": 0, "w": 16, "h": 6, "i": "2"}, "version": "2.19.5", "type": "visualization", "id": "api-waf-blocked", "embeddableConfig": {}},
+ {"panelIndex": "3", "gridData": {"x": 32, "y": 0, "w": 16, "h": 6, "i": "3"}, "version": "2.19.5", "type": "visualization", "id": "api-bf-count", "embeddableConfig": {}},
+ # Timeline
+ {"panelIndex": "4", "gridData": {"x": 0, "y": 6, "w": 48, "h": 12, "i": "4"}, "version": "2.19.5", "type": "visualization", "id": "api-waf-timeline", "embeddableConfig": {}},
+ # Top endpoints + Top IPs
+ {"panelIndex": "5", "gridData": {"x": 0, "y": 18, "w": 28, "h": 16, "i": "5"}, "version": "2.19.5", "type": "visualization", "id": "api-top-endpoints", "embeddableConfig": {}},
+ {"panelIndex": "6", "gridData": {"x": 28, "y": 18, "w": 20, "h": 16, "i": "6"}, "version": "2.19.5", "type": "visualization", "id": "api-top-ips", "embeddableConfig": {}},
+ # Codes HTTP + bloqué/passé + règles CRS
+ {"panelIndex": "7", "gridData": {"x": 0, "y": 34, "w": 16, "h": 14, "i": "7"}, "version": "2.19.5", "type": "visualization", "id": "api-http-codes", "embeddableConfig": {}},
+ {"panelIndex": "8", "gridData": {"x": 16, "y": 34, "w": 16, "h": 14, "i": "8"}, "version": "2.19.5", "type": "visualization", "id": "api-blocked-ratio", "embeddableConfig": {}},
+ {"panelIndex": "9", "gridData": {"x": 32, "y": 34, "w": 16, "h": 14, "i": "9"}, "version": "2.19.5", "type": "visualization", "id": "api-crs-rules", "embeddableConfig": {}},
+]
+
+# ── Main ──────────────────────────────────────────────────────────────────────
+def main():
+ print("=== Creation dashboard: Securite - API Backend & WAF ===\n")
+
+ for viz in VISUALIZATIONS:
+ vis_state = dict(viz["visState"])
+ vis_state["title"] = viz["title"]
+ body = {
+ "attributes": {
+ "title": viz["title"],
+ "visState": json.dumps(vis_state),
+ "uiStateJSON": viz.get("uiStateJSON", "{}"),
+ "description": "",
+ "kibanaSavedObjectMeta": {"searchSourceJSON": search_src(viz["query"])},
+ }
+ }
+ r = api("POST", f"/api/saved_objects/visualization/{viz['id']}?overwrite=true", body)
+ if "id" in r:
+ print(f" OK {viz['title']}")
+ else:
+ print(f" ERR {viz['title']}: {r}")
+ sys.exit(1)
+
+ dashboard_body = {
+ "attributes": {
+ "title": "Securite - API Backend et WAF",
+ "description": "ModSecurity WAF alertes et blocages, endpoints cibles, top IPs, codes HTTP, regles CRS",
+ "panelsJSON": json.dumps(PANELS),
+ "optionsJSON": json.dumps({"useMargins": True, "hidePanelTitles": False}),
+ "timeRestore": False,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": json.dumps({"query": {"language": "kuery", "query": ""}, "filter": []})
+ },
+ }
+ }
+ r = api("POST", "/api/saved_objects/dashboard/api-backend-waf-dashboard?overwrite=true", dashboard_body)
+ if "id" in r:
+ print(f"\n OK Dashboard: {r['id']}")
+ print(f"\n URL: https:///#/app/dashboards#/view/{r['id']}")
+ else:
+ print(f"\n ERR Dashboard: {r}")
+ sys.exit(1)
+
+ print("\n=== Termine ===")
+
+if __name__ == "__main__":
+ main()
diff --git a/monitoring/security/wazuh/create-dashboard-go-api.py b/monitoring/security/wazuh/create-dashboard-go-api.py
new file mode 100644
index 00000000..e9e43b77
--- /dev/null
+++ b/monitoring/security/wazuh/create-dashboard-go-api.py
@@ -0,0 +1,417 @@
+#!/usr/bin/env python3
+"""
+Crée le dashboard "API Go - Performance & Trafic" dans OpenSearch Dashboards (Wazuh).
+
+Usage:
+ docker cp create-dashboard-go-api.py wazuh_dashboard:/tmp/
+ docker exec wazuh_dashboard python3 /tmp/create-dashboard-go-api.py
+
+Données utilisées :
+ - rule 100700 → toute ligne backend Go
+ - rule 100701 → requête HTTP Gin (tous codes)
+ - rule 100702 → erreur 4xx
+ - rule 100703 → erreur 5xx
+ - rule 100704 → panic / fatal
+ - fields: data.id (code HTTP), data.extra_data (temps réponse),
+ data.srcip (IP client), data.protocol (GET/POST...),
+ data.url (endpoint), agent.name (prod-mln / pre-prod-mln)
+"""
+
+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!")
+INDEX_PATTERN = "wazuh-alerts-*"
+
+# ── Queries ────────────────────────────────────────────────────────────────────
+Q_ALL = "rule.id: 100700 OR rule.id: 100701 OR rule.id: 100702 OR rule.id: 100703 OR rule.id: 100704"
+Q_HTTP = "rule.id: 100701 OR rule.id: 100702 OR rule.id: 100703"
+Q_4XX = "rule.id: 100702"
+Q_5XX = "rule.id: 100703"
+Q_PANIC = "rule.id: 100704"
+
+# ── 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 search_src(query):
+ return json.dumps({
+ "index": INDEX_PATTERN,
+ "filter": [],
+ "query": {"language": "kuery", "query": query},
+ })
+
+# ── Visualisations ─────────────────────────────────────────────────────────────
+VISUALIZATIONS = [
+ # ── 1. Métrique : total requêtes HTTP ─────────────────────────────────────
+ {
+ "id": "go-api-total-requests",
+ "title": "[API Go] Total Requetes HTTP",
+ "type": "metric",
+ "query": Q_HTTP,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False, "useRanges": False,
+ "colorSchema": "Blues",
+ "metricColorMode": "None",
+ "colorsRange": [{"from": 0, "to": 99999999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False,
+ "subText": "requetes loggees", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ # ── 2. Métrique : erreurs 4xx ─────────────────────────────────────────────
+ {
+ "id": "go-api-4xx",
+ "title": "[API Go] Erreurs 4xx",
+ "type": "metric",
+ "query": Q_4XX,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False, "useRanges": False,
+ "colorSchema": "Yellow to Red",
+ "metricColorMode": "Labels",
+ "colorsRange": [{"from": 0, "to": 50}, {"from": 50, "to": 500}, {"from": 500, "to": 99999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False,
+ "subText": "erreurs client 4xx", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ # ── 3. Métrique : erreurs 5xx ─────────────────────────────────────────────
+ {
+ "id": "go-api-5xx",
+ "title": "[API Go] Erreurs 5xx",
+ "type": "metric",
+ "query": Q_5XX,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False, "useRanges": False,
+ "colorSchema": "Green to Red",
+ "metricColorMode": "Labels",
+ "colorsRange": [{"from": 0, "to": 1}, {"from": 1, "to": 20}, {"from": 20, "to": 99999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False,
+ "subText": "erreurs serveur 5xx", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ # ── 4. Timeline : requêtes par heure (prod vs pre-prod) ───────────────────
+ {
+ "id": "go-api-timeline",
+ "title": "[API Go] Timeline Requetes par Serveur",
+ "type": "histogram",
+ "query": Q_HTTP,
+ "visState": {
+ "type": "histogram",
+ "params": {
+ "type": "histogram",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "bottom",
+ "show": True, "style": {},
+ "labels": {"show": True, "truncate": 100}, "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value",
+ "position": "left", "show": True, "style": {},
+ "scale": {"type": "linear", "mode": "normal"},
+ "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100},
+ "title": {"text": "Requetes"}}],
+ "seriesParams": [{"show": True, "type": "histogram", "mode": "stacked",
+ "data": {"label": "Requetes", "id": "1"},
+ "valueAxis": "ValueAxis-1"}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right",
+ "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "date_histogram", "schema": "segment",
+ "params": {"field": "@timestamp", "interval": "auto", "min_doc_count": 1, "extended_bounds": {}}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "group",
+ "params": {"field": "agent.name", "size": 3, "order": "desc", "orderBy": "1",
+ "otherBucket": False}},
+ ],
+ },
+ },
+ # ── 5. Pie : distribution codes HTTP ──────────────────────────────────────
+ {
+ "id": "go-api-http-codes",
+ "title": "[API Go] Distribution Codes HTTP",
+ "type": "pie",
+ "query": Q_HTTP,
+ "visState": {
+ "type": "pie",
+ "params": {
+ "type": "pie", "addTooltip": True, "addLegend": True,
+ "legendPosition": "right", "isDonut": True,
+ "labels": {"show": True, "values": True, "last_level": True, "truncate": 100},
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment",
+ "params": {"field": "data.id", "size": 10, "order": "desc", "orderBy": "1",
+ "otherBucket": True, "otherBucketLabel": "Autres", "missingBucket": False}},
+ ],
+ },
+ },
+ # ── 6. Pie : méthodes HTTP ────────────────────────────────────────────────
+ {
+ "id": "go-api-methods",
+ "title": "[API Go] Methodes HTTP",
+ "type": "pie",
+ "query": Q_HTTP,
+ "visState": {
+ "type": "pie",
+ "params": {
+ "type": "pie", "addTooltip": True, "addLegend": True,
+ "legendPosition": "right", "isDonut": False,
+ "labels": {"show": True, "values": True, "last_level": True, "truncate": 100},
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment",
+ "params": {"field": "data.protocol", "size": 8, "order": "desc", "orderBy": "1",
+ "otherBucket": False, "missingBucket": False}},
+ ],
+ },
+ },
+ # ── 7. Bar horizontal : top endpoints ────────────────────────────────────
+ {
+ "id": "go-api-top-endpoints",
+ "title": "[API Go] Top Endpoints",
+ "type": "horizontal_bar",
+ "query": Q_HTTP,
+ "visState": {
+ "type": "horizontal_bar",
+ "params": {
+ "type": "horizontal_bar",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "left",
+ "show": True, "style": {},
+ "labels": {"show": True, "rotate": 0, "filter": True, "truncate": 300},
+ "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value",
+ "position": "bottom", "show": True, "style": {},
+ "scale": {"type": "linear", "mode": "normal"},
+ "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100},
+ "title": {"text": "Requetes"}}],
+ "seriesParams": [{"show": True, "type": "horizontal_bar", "mode": "stacked",
+ "data": {"label": "Requetes", "id": "1"},
+ "valueAxis": "ValueAxis-1"}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right",
+ "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment",
+ "params": {"field": "data.url", "size": 15, "order": "desc", "orderBy": "1",
+ "otherBucket": True, "otherBucketLabel": "Autres", "missingBucket": False}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "group",
+ "params": {"field": "data.id", "size": 4, "order": "desc", "orderBy": "1",
+ "otherBucket": True, "otherBucketLabel": "Autres"}},
+ ],
+ },
+ },
+ # ── 8. Bar horizontal : top endpoints en erreur ───────────────────────────
+ {
+ "id": "go-api-error-endpoints",
+ "title": "[API Go] Endpoints en Erreur (4xx/5xx)",
+ "type": "horizontal_bar",
+ "query": Q_4XX + " OR " + Q_5XX,
+ "visState": {
+ "type": "horizontal_bar",
+ "params": {
+ "type": "horizontal_bar",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "left",
+ "show": True, "style": {},
+ "labels": {"show": True, "rotate": 0, "filter": True, "truncate": 300},
+ "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value",
+ "position": "bottom", "show": True, "style": {},
+ "scale": {"type": "linear", "mode": "normal"},
+ "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100},
+ "title": {"text": "Erreurs"}}],
+ "seriesParams": [{"show": True, "type": "horizontal_bar", "mode": "stacked",
+ "data": {"label": "Erreurs", "id": "1"},
+ "valueAxis": "ValueAxis-1"}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right",
+ "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment",
+ "params": {"field": "data.url", "size": 15, "order": "desc", "orderBy": "1",
+ "otherBucket": True, "otherBucketLabel": "Autres", "missingBucket": False}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "group",
+ "params": {"field": "data.id", "size": 3, "order": "desc", "orderBy": "1",
+ "otherBucket": False}},
+ ],
+ },
+ },
+ # ── 9. Bar horizontal : top IPs clientes ──────────────────────────────────
+ {
+ "id": "go-api-top-clients",
+ "title": "[API Go] Top IPs Clientes",
+ "type": "horizontal_bar",
+ "query": Q_HTTP,
+ "visState": {
+ "type": "horizontal_bar",
+ "params": {
+ "type": "horizontal_bar",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "left",
+ "show": True, "style": {},
+ "labels": {"show": True, "rotate": 0, "filter": True, "truncate": 200},
+ "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value",
+ "position": "bottom", "show": True, "style": {},
+ "scale": {"type": "linear", "mode": "normal"},
+ "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100},
+ "title": {"text": "Requetes"}}],
+ "seriesParams": [{"show": True, "type": "horizontal_bar", "mode": "normal",
+ "data": {"label": "Requetes", "id": "1"},
+ "valueAxis": "ValueAxis-1"}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right",
+ "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment",
+ "params": {"field": "data.srcip", "size": 15, "order": "desc", "orderBy": "1",
+ "otherBucket": False, "missingBucket": False}},
+ ],
+ },
+ },
+ # ── 10. Table : erreurs critiques (panic / fatal) ──────────────────────────
+ {
+ "id": "go-api-panics",
+ "title": "[API Go] Erreurs Critiques (panic/fatal)",
+ "type": "table",
+ "query": Q_PANIC,
+ "visState": {
+ "type": "table",
+ "params": {
+ "perPage": 15, "showPartialRows": False, "showMetricsAtAllLevels": False,
+ "sort": {"columnIndex": None, "direction": None},
+ "showTotal": False, "totalFunc": "sum",
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "bucket",
+ "params": {"field": "agent.name", "size": 5, "order": "desc", "orderBy": "1",
+ "otherBucket": False, "missingBucket": False}},
+ {"id": "3", "enabled": True, "type": "date_histogram", "schema": "bucket",
+ "params": {"field": "@timestamp", "interval": "1d", "min_doc_count": 1, "extended_bounds": {}}},
+ ],
+ },
+ "uiStateJSON": json.dumps({"vis": {"params": {"sort": {"columnIndex": 0, "direction": "desc"}}}}),
+ },
+]
+
+# ── Layout dashboard (grille 48 colonnes) ──────────────────────────────────────
+# Ligne 0 : 3 métriques (total | 4xx | 5xx)
+# Ligne 6 : timeline requêtes (pleine largeur)
+# Ligne 18 : pie codes HTTP + pie méthodes
+# Ligne 34 : top endpoints (pleine largeur)
+# Ligne 50 : endpoints en erreur (gauche) + top clients (droite)
+# Ligne 66 : table panics (pleine largeur)
+PANELS = [
+ {"panelIndex": "1", "gridData": {"x": 0, "y": 0, "w": 16, "h": 6, "i": "1"}, "version": "2.19.5", "type": "visualization", "id": "go-api-total-requests", "embeddableConfig": {}},
+ {"panelIndex": "2", "gridData": {"x": 16, "y": 0, "w": 16, "h": 6, "i": "2"}, "version": "2.19.5", "type": "visualization", "id": "go-api-4xx", "embeddableConfig": {}},
+ {"panelIndex": "3", "gridData": {"x": 32, "y": 0, "w": 16, "h": 6, "i": "3"}, "version": "2.19.5", "type": "visualization", "id": "go-api-5xx", "embeddableConfig": {}},
+ {"panelIndex": "4", "gridData": {"x": 0, "y": 6, "w": 48, "h": 12, "i": "4"}, "version": "2.19.5", "type": "visualization", "id": "go-api-timeline", "embeddableConfig": {}},
+ {"panelIndex": "5", "gridData": {"x": 0, "y": 18, "w": 24, "h": 16, "i": "5"}, "version": "2.19.5", "type": "visualization", "id": "go-api-http-codes", "embeddableConfig": {}},
+ {"panelIndex": "6", "gridData": {"x": 24, "y": 18, "w": 24, "h": 16, "i": "6"}, "version": "2.19.5", "type": "visualization", "id": "go-api-methods", "embeddableConfig": {}},
+ {"panelIndex": "7", "gridData": {"x": 0, "y": 34, "w": 48, "h": 16, "i": "7"}, "version": "2.19.5", "type": "visualization", "id": "go-api-top-endpoints", "embeddableConfig": {}},
+ {"panelIndex": "8", "gridData": {"x": 0, "y": 50, "w": 28, "h": 16, "i": "8"}, "version": "2.19.5", "type": "visualization", "id": "go-api-error-endpoints", "embeddableConfig": {}},
+ {"panelIndex": "9", "gridData": {"x": 28, "y": 50, "w": 20, "h": 16, "i": "9"}, "version": "2.19.5", "type": "visualization", "id": "go-api-top-clients", "embeddableConfig": {}},
+ {"panelIndex": "10", "gridData": {"x": 0, "y": 66, "w": 48, "h": 12, "i": "10"}, "version": "2.19.5", "type": "visualization", "id": "go-api-panics", "embeddableConfig": {}},
+]
+
+# ── Main ───────────────────────────────────────────────────────────────────────
+def main():
+ print("=== Creation dashboard: API Go - Performance & Trafic ===\n")
+
+ for viz in VISUALIZATIONS:
+ vis_state = dict(viz["visState"])
+ vis_state["title"] = viz["title"]
+ body = {
+ "attributes": {
+ "title": viz["title"],
+ "visState": json.dumps(vis_state),
+ "uiStateJSON": viz.get("uiStateJSON", "{}"),
+ "description": "",
+ "kibanaSavedObjectMeta": {"searchSourceJSON": search_src(viz["query"])},
+ }
+ }
+ r = api("POST", f"/api/saved_objects/visualization/{viz['id']}?overwrite=true", body)
+ if "id" in r:
+ print(f" OK {viz['title']}")
+ else:
+ print(f" ERR {viz['title']}: {r}")
+ sys.exit(1)
+
+ dashboard_body = {
+ "attributes": {
+ "title": "API Go - Performance et Trafic",
+ "description": "Requetes HTTP Gin, codes de statut, endpoints, temps de reponse, erreurs 4xx/5xx, panics",
+ "panelsJSON": json.dumps(PANELS),
+ "optionsJSON": json.dumps({"useMargins": True, "hidePanelTitles": False}),
+ "timeRestore": False,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": json.dumps({"query": {"language": "kuery", "query": ""}, "filter": []})
+ },
+ }
+ }
+ r = api("POST", "/api/saved_objects/dashboard/go-api-performance-dashboard?overwrite=true", dashboard_body)
+ if "id" in r:
+ print(f"\n OK Dashboard: {r['id']}")
+ print(f" URL: https:///#/app/dashboards#/view/{r['id']}")
+ else:
+ print(f"\n ERR Dashboard: {r}")
+ sys.exit(1)
+
+ print("\n=== Termine ===")
+
+if __name__ == "__main__":
+ main()
diff --git a/monitoring/security/wazuh/create-dashboard-vpn.py b/monitoring/security/wazuh/create-dashboard-vpn.py
new file mode 100644
index 00000000..e8e04bab
--- /dev/null
+++ b/monitoring/security/wazuh/create-dashboard-vpn.py
@@ -0,0 +1,357 @@
+#!/usr/bin/env python3
+"""
+Crée le dashboard "VPN - Activite WireGuard" dans OpenSearch Dashboards (Wazuh).
+
+Usage:
+ docker cp create-dashboard-vpn.py wazuh_dashboard:/tmp/
+ docker exec wazuh_dashboard python3 /tmp/create-dashboard-vpn.py
+
+Données utilisées :
+ - rules 100801-100803 → handshakes WireGuard
+ - rules 100810-100812 → timeouts / déconnexions
+ - rule 100815 → peer roaming
+ - fields: data.srcip (IP peer), data.id (peer number), data.extra_data (retry)
+ - agent.name: vpn-prod
+"""
+
+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!")
+INDEX_PATTERN = "wazuh-alerts-*"
+
+# ── Queries ────────────────────────────────────────────────────────────────────
+Q_ALL = "rule.id: 100801 OR rule.id: 100802 OR rule.id: 100803 OR rule.id: 100810 OR rule.id: 100811 OR rule.id: 100812 OR rule.id: 100815"
+Q_HANDSHAKES = "rule.id: 100801 OR rule.id: 100802 OR rule.id: 100803"
+Q_WARNINGS = "rule.id: 100810 OR rule.id: 100811 OR rule.id: 100812"
+Q_COMPLETE = "rule.id: 100803"
+
+# ── 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 search_src(query):
+ return json.dumps({
+ "index": INDEX_PATTERN,
+ "filter": [],
+ "query": {"language": "kuery", "query": query},
+ })
+
+# ── Visualisations ─────────────────────────────────────────────────────────────
+VISUALIZATIONS = [
+ # ── 1. Métrique : total handshakes (24h) ──────────────────────────────────
+ {
+ "id": "vpn-total-handshakes",
+ "title": "[VPN] Handshakes (24h)",
+ "type": "metric",
+ "query": Q_HANDSHAKES,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False, "useRanges": False,
+ "colorSchema": "Green to Red",
+ "metricColorMode": "None",
+ "colorsRange": [{"from": 0, "to": 9999999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False,
+ "subText": "handshakes VPN", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ # ── 2. Métrique : alertes déconnexion ─────────────────────────────────────
+ {
+ "id": "vpn-disconnects",
+ "title": "[VPN] Alertes Deconnexion",
+ "type": "metric",
+ "query": Q_WARNINGS,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False, "useRanges": False,
+ "colorSchema": "Green to Red",
+ "metricColorMode": "Labels",
+ "colorsRange": [{"from": 0, "to": 1}, {"from": 1, "to": 10}, {"from": 10, "to": 99999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False,
+ "subText": "timeouts / deconnexions", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ # ── 3. Métrique : peers actifs distincts ──────────────────────────────────
+ {
+ "id": "vpn-active-peers",
+ "title": "[VPN] Peers Actifs (distincts)",
+ "type": "metric",
+ "query": Q_COMPLETE,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False, "useRanges": False,
+ "colorSchema": "Blues",
+ "metricColorMode": "None",
+ "colorsRange": [{"from": 0, "to": 99999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False,
+ "subText": "IPs peers uniques", "fontSize": 60},
+ }
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "cardinality", "schema": "metric",
+ "params": {"field": "data.srcip"}},
+ ],
+ },
+ },
+ # ── 4. Timeline : activité WireGuard ──────────────────────────────────────
+ {
+ "id": "vpn-timeline",
+ "title": "[VPN] Timeline Activite WireGuard",
+ "type": "histogram",
+ "query": Q_ALL,
+ "visState": {
+ "type": "histogram",
+ "params": {
+ "type": "histogram",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "bottom",
+ "show": True, "style": {}, "scale": {"type": "linear"},
+ "labels": {"show": True, "truncate": 100}, "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value",
+ "position": "left", "show": True, "style": {},
+ "scale": {"type": "linear", "mode": "normal"},
+ "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100},
+ "title": {"text": "Evenements"}}],
+ "seriesParams": [{"show": True, "type": "histogram", "mode": "stacked",
+ "data": {"label": "Evenements", "id": "1"},
+ "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": True,
+ "showCircles": True}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right",
+ "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "date_histogram", "schema": "segment",
+ "params": {"field": "@timestamp", "interval": "auto", "min_doc_count": 1, "extended_bounds": {}}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "group",
+ "params": {"field": "rule.description", "size": 5, "order": "desc", "orderBy": "1",
+ "otherBucket": True, "otherBucketLabel": "Autres"}},
+ ],
+ },
+ },
+ # ── 5. Bar horizontal : événements par peer (IP) ──────────────────────────
+ {
+ "id": "vpn-events-per-peer",
+ "title": "[VPN] Evenements par Peer",
+ "type": "horizontal_bar",
+ "query": Q_ALL,
+ "visState": {
+ "type": "horizontal_bar",
+ "params": {
+ "type": "horizontal_bar",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "left",
+ "show": True, "style": {},
+ "labels": {"show": True, "rotate": 0, "filter": True, "truncate": 200},
+ "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value",
+ "position": "bottom", "show": True, "style": {},
+ "scale": {"type": "linear", "mode": "normal"},
+ "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100},
+ "title": {"text": "Evenements"}}],
+ "seriesParams": [{"show": True, "type": "horizontal_bar", "mode": "stacked",
+ "data": {"label": "Evenements", "id": "1"},
+ "valueAxis": "ValueAxis-1"}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right",
+ "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment",
+ "params": {"field": "data.srcip", "size": 10, "order": "desc", "orderBy": "1",
+ "otherBucket": False, "missingBucket": False}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "group",
+ "params": {"field": "rule.description", "size": 4, "order": "desc", "orderBy": "1",
+ "otherBucket": True, "otherBucketLabel": "Autres"}},
+ ],
+ },
+ },
+ # ── 6. Pie : types d'événements ───────────────────────────────────────────
+ {
+ "id": "vpn-event-types",
+ "title": "[VPN] Types d Evenements",
+ "type": "pie",
+ "query": Q_ALL,
+ "visState": {
+ "type": "pie",
+ "params": {
+ "type": "pie", "addTooltip": True, "addLegend": True,
+ "legendPosition": "right", "isDonut": True,
+ "labels": {"show": True, "values": True, "last_level": True, "truncate": 100},
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment",
+ "params": {"field": "rule.description", "size": 8, "order": "desc", "orderBy": "1",
+ "otherBucket": True, "otherBucketLabel": "Autres", "missingBucket": False}},
+ ],
+ },
+ },
+ # ── 7. Timeline : timeouts et déconnexions ────────────────────────────────
+ {
+ "id": "vpn-timeout-timeline",
+ "title": "[VPN] Timeouts et Deconnexions",
+ "type": "histogram",
+ "query": Q_WARNINGS,
+ "visState": {
+ "type": "histogram",
+ "params": {
+ "type": "histogram",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "bottom",
+ "show": True, "style": {},
+ "labels": {"show": True, "truncate": 100}, "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value",
+ "position": "left", "show": True, "style": {},
+ "scale": {"type": "linear", "mode": "normal"},
+ "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100},
+ "title": {"text": "Alertes"}}],
+ "seriesParams": [{"show": True, "type": "histogram", "mode": "stacked",
+ "data": {"label": "Alertes", "id": "1"},
+ "valueAxis": "ValueAxis-1"}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right",
+ "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "date_histogram", "schema": "segment",
+ "params": {"field": "@timestamp", "interval": "auto", "min_doc_count": 1, "extended_bounds": {}}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "group",
+ "params": {"field": "data.srcip", "size": 6, "order": "desc", "orderBy": "1",
+ "otherBucket": False}},
+ ],
+ },
+ },
+ # ── 8. Table : log des événements VPN ─────────────────────────────────────
+ {
+ "id": "vpn-events-table",
+ "title": "[VPN] Journal Evenements VPN",
+ "type": "table",
+ "query": Q_ALL,
+ "visState": {
+ "type": "table",
+ "params": {
+ "perPage": 20, "showPartialRows": False, "showMetricsAtAllLevels": False,
+ "sort": {"columnIndex": None, "direction": None},
+ "showTotal": False, "totalFunc": "sum",
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "bucket",
+ "params": {"field": "data.srcip", "size": 20, "order": "desc", "orderBy": "1",
+ "otherBucket": False, "missingBucket": True, "missingBucketLabel": "inconnu"}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "bucket",
+ "params": {"field": "rule.description", "size": 5, "order": "desc", "orderBy": "1",
+ "otherBucket": False, "missingBucket": False}},
+ ],
+ },
+ "uiStateJSON": json.dumps({"vis": {"params": {"sort": {"columnIndex": 0, "direction": "desc"}}}}),
+ },
+]
+
+# ── Layout dashboard (grille 48 colonnes) ──────────────────────────────────────
+# Ligne 0 : 3 métriques
+# Ligne 6 : timeline activité (pleine largeur)
+# Ligne 18: events par peer (gauche) + types d'événements (droite)
+# Ligne 34: timeline timeouts (gauche) + table journal (droite)
+PANELS = [
+ {"panelIndex": "1", "gridData": {"x": 0, "y": 0, "w": 16, "h": 6, "i": "1"}, "version": "2.19.5", "type": "visualization", "id": "vpn-total-handshakes", "embeddableConfig": {}},
+ {"panelIndex": "2", "gridData": {"x": 16, "y": 0, "w": 16, "h": 6, "i": "2"}, "version": "2.19.5", "type": "visualization", "id": "vpn-disconnects", "embeddableConfig": {}},
+ {"panelIndex": "3", "gridData": {"x": 32, "y": 0, "w": 16, "h": 6, "i": "3"}, "version": "2.19.5", "type": "visualization", "id": "vpn-active-peers", "embeddableConfig": {}},
+ {"panelIndex": "4", "gridData": {"x": 0, "y": 6, "w": 48, "h": 12, "i": "4"}, "version": "2.19.5", "type": "visualization", "id": "vpn-timeline", "embeddableConfig": {}},
+ {"panelIndex": "5", "gridData": {"x": 0, "y": 18, "w": 30, "h": 16, "i": "5"}, "version": "2.19.5", "type": "visualization", "id": "vpn-events-per-peer", "embeddableConfig": {}},
+ {"panelIndex": "6", "gridData": {"x": 30, "y": 18, "w": 18, "h": 16, "i": "6"}, "version": "2.19.5", "type": "visualization", "id": "vpn-event-types", "embeddableConfig": {}},
+ {"panelIndex": "7", "gridData": {"x": 0, "y": 34, "w": 24, "h": 14, "i": "7"}, "version": "2.19.5", "type": "visualization", "id": "vpn-timeout-timeline", "embeddableConfig": {}},
+ {"panelIndex": "8", "gridData": {"x": 24, "y": 34, "w": 24, "h": 14, "i": "8"}, "version": "2.19.5", "type": "visualization", "id": "vpn-events-table", "embeddableConfig": {}},
+]
+
+# ── Main ───────────────────────────────────────────────────────────────────────
+def main():
+ print("=== Creation dashboard: VPN - Activite WireGuard ===\n")
+
+ for viz in VISUALIZATIONS:
+ vis_state = dict(viz["visState"])
+ vis_state["title"] = viz["title"]
+ body = {
+ "attributes": {
+ "title": viz["title"],
+ "visState": json.dumps(vis_state),
+ "uiStateJSON": viz.get("uiStateJSON", "{}"),
+ "description": "",
+ "kibanaSavedObjectMeta": {"searchSourceJSON": search_src(viz["query"])},
+ }
+ }
+ r = api("POST", f"/api/saved_objects/visualization/{viz['id']}?overwrite=true", body)
+ if "id" in r:
+ print(f" OK {viz['title']}")
+ else:
+ print(f" ERR {viz['title']}: {r}")
+ sys.exit(1)
+
+ dashboard_body = {
+ "attributes": {
+ "title": "VPN - Activite WireGuard",
+ "description": "Handshakes peers, deconnexions, timeouts, activite par IP — agent vpn-prod",
+ "panelsJSON": json.dumps(PANELS),
+ "optionsJSON": json.dumps({"useMargins": True, "hidePanelTitles": False}),
+ "timeRestore": False,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": json.dumps({"query": {"language": "kuery", "query": ""}, "filter": []})
+ },
+ }
+ }
+ r = api("POST", "/api/saved_objects/dashboard/vpn-wireguard-dashboard?overwrite=true", dashboard_body)
+ if "id" in r:
+ print(f"\n OK Dashboard: {r['id']}")
+ print(f" URL: https:///#/app/dashboards#/view/{r['id']}")
+ else:
+ print(f"\n ERR Dashboard: {r}")
+ sys.exit(1)
+
+ print("\n=== Termine ===")
+
+if __name__ == "__main__":
+ main()
diff --git a/monitoring/security/wazuh/create-dashboard.py b/monitoring/security/wazuh/create-dashboard.py
new file mode 100644
index 00000000..d4330669
--- /dev/null
+++ b/monitoring/security/wazuh/create-dashboard.py
@@ -0,0 +1,255 @@
+#!/usr/bin/env python3
+"""
+Crée le dashboard "Securite - Alertes Web et CrowdSec" dans OpenSearch Dashboards (Wazuh).
+
+Usage:
+ python3 create-dashboard.py
+
+Le script se connecte depuis l'intérieur du container wazuh_dashboard via:
+ docker exec wazuh_dashboard python3 /tmp/create-dashboard.py
+
+Ou directement si OpenSearch Dashboards est accessible sur localhost:5601.
+"""
+
+import json
+import urllib.request
+import urllib.error
+import ssl
+import base64
+import subprocess
+import sys
+import os
+
+# ── Configuration ─────────────────────────────────────────────────────────────
+DASHBOARD_HOST = "https://localhost:5601"
+DASHBOARD_USER = "kibanaserver"
+DASHBOARD_PASS = os.environ.get("DASHBOARD_PASSWORD", "E9Jpr6586kQ3wYrCS2!")
+INDEX_PATTERN = "wazuh-alerts-*"
+
+# ── Queries de filtre ─────────────────────────────────────────────────────────
+Q_WEB = "rule.groups: web OR rule.groups: attack OR rule.groups: modsecurity OR rule.groups: api_brute_force"
+Q_CS = "rule.groups: crowdsec OR rule.groups: crowdsec_ban"
+Q_ALL = Q_WEB + " OR " + Q_CS
+
+# ── 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 search_src(query):
+ return json.dumps({
+ "index": INDEX_PATTERN,
+ "filter": [],
+ "query": {"language": "kuery", "query": query},
+ })
+
+# ── Définition des visualisations ─────────────────────────────────────────────
+VISUALIZATIONS = [
+ {
+ "id": "sec-web-count",
+ "title": "[Securite] Alertes Web - Total",
+ "type": "metric",
+ "query": Q_WEB,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False,
+ "useRanges": False,
+ "colorSchema": "Green to Red",
+ "metricColorMode": "Labels",
+ "colorsRange": [{"from": 0, "to": 10}, {"from": 10, "to": 100}, {"from": 100, "to": 9999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False, "subText": "alertes web", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ {
+ "id": "sec-cs-count",
+ "title": "[Securite] CrowdSec - Total Bans",
+ "type": "metric",
+ "query": Q_CS,
+ "visState": {
+ "type": "metric",
+ "params": {
+ "metric": {
+ "percentageMode": False,
+ "useRanges": False,
+ "colorSchema": "Green to Red",
+ "metricColorMode": "Labels",
+ "colorsRange": [{"from": 0, "to": 5}, {"from": 5, "to": 50}, {"from": 50, "to": 9999}],
+ "labels": {"show": True},
+ "invertColors": False,
+ "style": {"bgFill": "#000", "bgColor": False, "labelColor": False, "subText": "bans crowdsec", "fontSize": 60},
+ }
+ },
+ "aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
+ },
+ },
+ {
+ "id": "sec-web-timeline",
+ "title": "[Securite] Alertes Web - Timeline",
+ "type": "histogram",
+ "query": Q_WEB,
+ "visState": {
+ "type": "histogram",
+ "params": {
+ "type": "histogram",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "bottom", "show": True, "style": {}, "scale": {"type": "linear"}, "labels": {"show": True, "truncate": 100}, "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "left", "show": True, "style": {}, "scale": {"type": "linear", "mode": "normal"}, "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100}, "title": {"text": "Alertes"}}],
+ "seriesParams": [{"show": True, "type": "histogram", "mode": "stacked", "data": {"label": "Alertes", "id": "1"}, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": True, "showCircles": True}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right", "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "date_histogram", "schema": "segment", "params": {"field": "@timestamp", "interval": "auto", "customInterval": "2h", "min_doc_count": 1, "extended_bounds": {}}},
+ ],
+ },
+ },
+ {
+ "id": "sec-top-ips",
+ "title": "[Securite] Top IPs Attaquantes",
+ "type": "horizontal_bar",
+ "query": Q_ALL,
+ "visState": {
+ "type": "horizontal_bar",
+ "params": {
+ "type": "horizontal_bar",
+ "grid": {"categoryLines": False},
+ "categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "left", "show": True, "style": {}, "scale": {"type": "linear"}, "labels": {"show": True, "rotate": 0, "filter": True, "truncate": 200}, "title": {}}],
+ "valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "bottom", "show": True, "style": {}, "scale": {"type": "linear", "mode": "normal"}, "labels": {"show": True, "rotate": 75, "filter": False, "truncate": 100}, "title": {"text": "Alertes"}}],
+ "seriesParams": [{"show": True, "type": "horizontal_bar", "mode": "stacked", "data": {"label": "Alertes", "id": "1"}, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": True, "showCircles": True}],
+ "addTooltip": True, "addLegend": True, "legendPosition": "right", "times": [], "addTimeMarker": False,
+ },
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "data.srcip", "size": 15, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
+ ],
+ },
+ },
+ {
+ "id": "sec-top-rules",
+ "title": "[Securite] Top Regles Declenchees",
+ "type": "pie",
+ "query": Q_WEB,
+ "visState": {
+ "type": "pie",
+ "params": {"type": "pie", "addTooltip": True, "addLegend": True, "legendPosition": "right", "isDonut": True, "labels": {"show": False, "values": True, "last_level": True, "truncate": 100}},
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "rule.description", "size": 10, "order": "desc", "orderBy": "1", "otherBucket": True, "otherBucketLabel": "Autres", "missingBucket": False}},
+ ],
+ },
+ },
+ {
+ "id": "sec-cs-bans-table",
+ "title": "[Securite] CrowdSec - IPs Bannies",
+ "type": "table",
+ "query": Q_CS,
+ "visState": {
+ "type": "table",
+ "params": {"perPage": 20, "showPartialRows": False, "showMetricsAtAllLevels": False, "sort": {"columnIndex": None, "direction": None}, "showTotal": False, "totalFunc": "sum"},
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "bucket", "params": {"field": "data.srcip", "size": 30, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
+ {"id": "3", "enabled": True, "type": "terms", "schema": "bucket", "params": {"field": "rule.description", "size": 5, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
+ ],
+ },
+ "uiStateJSON": json.dumps({"vis": {"params": {"sort": {"columnIndex": 0, "direction": "desc"}}}}),
+ },
+ {
+ "id": "sec-severity",
+ "title": "[Securite] Distribution Severite",
+ "type": "pie",
+ "query": Q_WEB,
+ "visState": {
+ "type": "pie",
+ "params": {"type": "pie", "addTooltip": True, "addLegend": True, "legendPosition": "right", "isDonut": False, "labels": {"show": True, "values": True, "last_level": True, "truncate": 100}},
+ "aggs": [
+ {"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
+ {"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "rule.level", "size": 15, "order": "desc", "orderBy": "1", "otherBucket": False}},
+ ],
+ },
+ },
+]
+
+# ── Layout dashboard (grid 48 colonnes) ───────────────────────────────────────
+PANELS = [
+ {"panelIndex": "1", "gridData": {"x": 0, "y": 0, "w": 24, "h": 6, "i": "1"}, "version": "2.19.5", "type": "visualization", "id": "sec-web-count", "embeddableConfig": {}},
+ {"panelIndex": "2", "gridData": {"x": 24, "y": 0, "w": 24, "h": 6, "i": "2"}, "version": "2.19.5", "type": "visualization", "id": "sec-cs-count", "embeddableConfig": {}},
+ {"panelIndex": "3", "gridData": {"x": 0, "y": 6, "w": 48, "h": 12, "i": "3"}, "version": "2.19.5", "type": "visualization", "id": "sec-web-timeline", "embeddableConfig": {}},
+ {"panelIndex": "4", "gridData": {"x": 0, "y": 18, "w": 28, "h": 14, "i": "4"}, "version": "2.19.5", "type": "visualization", "id": "sec-top-ips", "embeddableConfig": {}},
+ {"panelIndex": "5", "gridData": {"x": 28, "y": 18, "w": 20, "h": 14, "i": "5"}, "version": "2.19.5", "type": "visualization", "id": "sec-top-rules", "embeddableConfig": {}},
+ {"panelIndex": "6", "gridData": {"x": 0, "y": 32, "w": 48, "h": 12, "i": "6"}, "version": "2.19.5", "type": "visualization", "id": "sec-cs-bans-table", "embeddableConfig": {}},
+ {"panelIndex": "7", "gridData": {"x": 0, "y": 44, "w": 24, "h": 10, "i": "7"}, "version": "2.19.5", "type": "visualization", "id": "sec-severity", "embeddableConfig": {}},
+]
+
+# ── Main ──────────────────────────────────────────────────────────────────────
+def main():
+ print("=== Création du dashboard Securite - Alertes Web et CrowdSec ===\n")
+
+ # Visualisations
+ for viz in VISUALIZATIONS:
+ vis_state = dict(viz["visState"])
+ vis_state["title"] = viz["title"]
+ body = {
+ "attributes": {
+ "title": viz["title"],
+ "visState": json.dumps(vis_state),
+ "uiStateJSON": viz.get("uiStateJSON", "{}"),
+ "description": "",
+ "kibanaSavedObjectMeta": {"searchSourceJSON": search_src(viz["query"])},
+ }
+ }
+ r = api("POST", f"/api/saved_objects/visualization/{viz['id']}?overwrite=true", body)
+ if "id" in r:
+ print(f" ✅ {viz['title']}")
+ else:
+ print(f" ❌ {viz['title']}: {r}")
+ sys.exit(1)
+
+ # Dashboard
+ dashboard_body = {
+ "attributes": {
+ "title": "Securite - Alertes Web et CrowdSec",
+ "description": "Alertes OWASP ModSecurity brute-force API et bans CrowdSec",
+ "panelsJSON": json.dumps(PANELS),
+ "optionsJSON": json.dumps({"useMargins": True, "hidePanelTitles": False}),
+ "timeRestore": False,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": json.dumps({"query": {"language": "kuery", "query": ""}, "filter": []})
+ },
+ }
+ }
+ r = api("POST", "/api/saved_objects/dashboard/sec-web-crowdsec-dashboard?overwrite=true", dashboard_body)
+ if "id" in r:
+ print(f"\n ✅ Dashboard: {r['id']}")
+ print(f"\n URL: https:///#/app/dashboards#/view/{r['id']}")
+ else:
+ print(f"\n ❌ Dashboard: {r}")
+ sys.exit(1)
+
+ print("\n=== Terminé ===")
+
+if __name__ == "__main__":
+ main()