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()