This commit is contained in:
2026-06-25 18:45:58 +02:00
parent 283190db09
commit 373074ccb2
14 changed files with 1828 additions and 16 deletions
@@ -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
@@ -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
@@ -0,0 +1,38 @@
<!-- Configuration centralisée distribuée aux agents Wazuh -->
<!-- Déposée dans /var/ossec/etc/shared/default/agent.conf sur le manager -->
<agent_config>
<!-- ── Logs nginx (prod-uber et pre-prod-uber) ──────────────────── -->
<localfile>
<log_format>apache</log_format>
<location>/var/log/nginx/access.log</location>
</localfile>
<localfile>
<log_format>syslog</log_format>
<location>/var/log/nginx/error.log</location>
</localfile>
<!-- ── Logs Docker backend Go (stdout container) ─────────────────── -->
<!-- Le container s'appelle "backend" dans le compose prod -->
<localfile>
<log_format>syslog</log_format>
<location>/var/lib/docker/containers/*/*-json.log</location>
<label key="container">backend</label>
</localfile>
<!-- ── Auth system ───────────────────────────────────────────────── -->
<localfile>
<log_format>syslog</log_format>
<location>/var/log/auth.log</location>
</localfile>
<!-- ── Syslog ────────────────────────────────────────────────────── -->
<localfile>
<log_format>syslog</log_format>
<location>/var/log/syslog</location>
</localfile>
</agent_config>
</content>
@@ -0,0 +1,29 @@
<!-- Decoders API Backend Go (service systemd "backend") -->
<!-- Root : capture tous les logs du service systemd backend -->
<decoder name="go-backend">
<program_name>backend</program_name>
</decoder>
<!-- Child : logs HTTP Gin — "[GIN] | 200 | 1.234ms | 1.2.3.4 | GET /api/..." -->
<decoder name="go-backend-gin">
<parent>go-backend</parent>
<prematch>[GIN]</prematch>
<regex type="pcre2">\|\s+(\d{3})\s+\|\s+(\S+)\s+\|\s+([\d.]+)\s+\|\s+(\S+)\s+(\S+)</regex>
<order>id,extra_data,srcip,protocol,url</order>
</decoder>
<!-- Child : echecs auth login -->
<decoder name="go-backend-login-fail">
<parent>go-backend</parent>
<prematch>LOGIN_ADMIN|LOGIN_CLIENT</prematch>
<regex>LOGIN_\S+ \S+ \S+ \S+: (\S+)</regex>
<order>srcuser</order>
</decoder>
<!-- Fallback root (logs non-systemd, ex: pre-prod Docker) -->
<decoder name="go-backend-raw">
<prematch>LOGIN_ADMIN|LOGIN_CLIENT</prematch>
<regex>LOGIN_\S+ \S+ \S+ \S+: (\S+)</regex>
<order>srcuser</order>
</decoder>
@@ -1,24 +1,38 @@
<!-- CrowdSec Decoders — format logrus key=value -->
<!-- CrowdSec Decoders — format logrus key=value
Le decoder natif aws-eks-authenticator est exclu dans ossec.conf
(<decoder_exclude>) car son prematch pcre2 capture aussi ce format.
Ce decoder root "crowdsec" prend donc la main. -->
<decoder name="crowdsec">
<prematch>^time="20\d\d-\d\d-\d\dT\d\d:\d\d:\d\d</prematch>
</decoder>
<decoder name="crowdsec-fields">
<parent>crowdsec</parent>
<regex>level=(\S+) msg="(\.+)"</regex>
<order>status,extra_data</order>
<prematch type="pcre2">^time="\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}</prematch>
</decoder>
<!-- Ban CrowdSec : format "... : Xh ban on Ip A.B.C.D" (module db) -->
<decoder name="crowdsec-ban">
<parent>crowdsec</parent>
<prematch>ban Ip |ban ip |type":"ban</prematch>
<regex>(\d+\.\d+\.\d+\.\d+)</regex>
<prematch>ban on Ip </prematch>
<regex type="pcre2">ban on Ip ([\d.]+)</regex>
<order>srcip</order>
</decoder>
<decoder name="crowdsec-overflow">
<!-- Ban CrowdSec : variante minuscule "ban on ip A.B.C.D" -->
<decoder name="crowdsec-ban-lc">
<parent>crowdsec</parent>
<prematch>overflow from |triggered |New overflow</prematch>
<regex>'(\d+\.\d+\.\d+\.\d+)'</regex>
<prematch>ban on ip </prematch>
<regex type="pcre2">ban on ip ([\d.]+)</regex>
<order>srcip</order>
</decoder>
<!-- Overflow CrowdSec -->
<decoder name="crowdsec-overflow">
<parent>crowdsec</parent>
<prematch>overflow from </prematch>
<regex type="pcre2">overflow from ([\d.]+)</regex>
<order>srcip</order>
</decoder>
<!-- Champs generaux level/msg pour les logs non-ban -->
<decoder name="crowdsec-fields">
<parent>crowdsec</parent>
<regex type="pcre2">level=(\S+) msg="(.+?)"</regex>
<order>status,extra_data</order>
</decoder>
@@ -0,0 +1,56 @@
<!-- WireGuard Decoders — logs kernel via journald
Format: wireguard: wg0: <action> to/from peer N (IP:port) -->
<!-- Root : messages kernel WireGuard (syslog program_name=kernel) -->
<decoder name="wireguard">
<program_name>kernel</program_name>
<prematch>wireguard: wg0: </prematch>
</decoder>
<!-- Initiation handshake : VPN server → peer -->
<decoder name="wireguard-handshake-init">
<parent>wireguard</parent>
<prematch>Sending handshake initiation</prematch>
<regex type="pcre2">wireguard: wg0: Sending handshake initiation to peer (\d+) \(([\d.]+):\d+\)</regex>
<order>id,srcip</order>
</decoder>
<!-- Réponse handshake envoyée : VPN server → peer -->
<decoder name="wireguard-handshake-resp-sent">
<parent>wireguard</parent>
<prematch>Sending handshake response</prematch>
<regex type="pcre2">wireguard: wg0: Sending handshake response to peer (\d+) \(([\d.]+):\d+\)</regex>
<order>id,srcip</order>
</decoder>
<!-- Réponse handshake reçue : peer → VPN server -->
<decoder name="wireguard-handshake-resp-recv">
<parent>wireguard</parent>
<prematch>Receiving handshake response</prematch>
<regex type="pcre2">wireguard: wg0: Receiving handshake response from peer (\d+) \(([\d.]+):\d+\)</regex>
<order>id,srcip</order>
</decoder>
<!-- Keepalive (niveau bas, pas d'alerte) -->
<decoder name="wireguard-keepalive">
<parent>wireguard</parent>
<prematch>keepalive packet</prematch>
<regex type="pcre2">wireguard: wg0: \S+ keepalive packet \S+ peer (\d+) \(([\d.]+):\d+\)</regex>
<order>id,srcip</order>
</decoder>
<!-- Handshake timeout — "did not complete after 5 seconds, retrying (try N)" -->
<decoder name="wireguard-timeout">
<parent>wireguard</parent>
<prematch>did not complete</prematch>
<regex type="pcre2">wireguard: wg0: Handshake for peer (\d+)[^(]+\(([\d.]+):\d+\).+retrying \(try (\d+)\)</regex>
<order>id,srcip,extra_data</order>
</decoder>
<!-- Peer roaming (changement d'IP source) -->
<decoder name="wireguard-roaming">
<parent>wireguard</parent>
<prematch>is now roaming</prematch>
<regex type="pcre2">wireguard: wg0: Peer (\d+).+roaming to ([\d.]+):\d+</regex>
<order>id,srcip</order>
</decoder>
@@ -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
@@ -773,11 +773,67 @@
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>5712,5720,5763</rules_id>
<rules_id>5710,5711,5712,5716,5720,5760,5763</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- Attaques web WAF/OWASP → CrowdSec ban 12h -->
<!-- SSH brute-force custom (100300) + scan comptes (100301) → CrowdSec ban 24h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>100300,100301</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP SQLi (100220,100290) → CrowdSec ban 24h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>100220,100290</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP Command Injection (100222,100292) → CrowdSec ban permanent -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>100222,100292</rules_id>
<timeout>0</timeout>
</active-response>
<!-- OWASP RCE/Exploit (100230) → CrowdSec ban 24h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>100230</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP XSS (100240,100241,100291) → CrowdSec ban 12h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>100240,100241,100291</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP LFI/RFI/Path Traversal (100250,100251,100252,100293) → CrowdSec ban 12h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>100250,100251,100252,100293</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP XXE (100260) → CrowdSec ban 12h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>100260</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP Scanner/SSRF/Upload (100270,100280,100281) → CrowdSec ban 12h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
@@ -785,7 +841,23 @@
<timeout>43200</timeout>
</active-response>
<!-- CrowdSec ban détecté dans ses propres logs → ban 24h -->
<!-- ModSec attaque soutenue (100210) → CrowdSec ban 24h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>100202,100203,100204,100205,100206,100210</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- API brute-force login (100610,100611,100612,100613) → CrowdSec ban 24h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
<rules_id>100610,100611,100612,100613</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- CrowdSec ban détecté dans ses propres logs → renforcer le ban 24h -->
<active-response>
<command>crowdsec-ban</command>
<location>local</location>
@@ -939,6 +1011,10 @@
<list>etc/lists/malicious-ioc/malicious-ip</list>
<list>etc/lists/malicious-ioc/malicious-domains</list>
<list>etc/lists/malicious-ioc/malware-hashes</list>
<!-- Exclusion du decoder natif aws-eks-authenticator : son prematch pcre2
time="\d{4}-\d{2}-\d{2}T..." capture aussi les logs logrus CrowdSec,
empechant notre decoder crowdsec-ban d'extraire data.srcip. -->
<decoder_exclude>0565-aws-eks-authenticator_decoders.xml</decoder_exclude>
<!-- Decoders et règles custom ModSecurity -->
<decoder_dir>etc/decoders</decoder_dir>
<rule_dir>etc/rules</rule_dir>
@@ -0,0 +1,90 @@
<!-- Regles API Backend Go — IDs 100600-100720 -->
<!-- ── Logs backend systemd ───────────────────────────────────────────── -->
<group name="api_backend,">
<!-- Catch-all : toute ligne du service backend (niveau 3 = indexe dans OpenSearch) -->
<rule id="100700" level="3">
<decoded_as>go-backend</decoded_as>
<description>Go Backend API: log entry</description>
<group>api_backend,</group>
</rule>
<!-- Log HTTP Gin : toute requete -->
<rule id="100701" level="3">
<if_sid>100700</if_sid>
<match>[GIN]</match>
<description>API: Requete HTTP Gin loggee</description>
<group>api_backend,api_http,</group>
</rule>
<!-- Log HTTP Gin : erreur client 4xx — pattern "| 4NN |" -->
<rule id="100702" level="5">
<if_sid>100701</if_sid>
<regex>| 4\d\d |</regex>
<description>API: Erreur client 4xx — $(srcip)</description>
<group>api_backend,api_http,api_error,</group>
</rule>
<!-- Log HTTP Gin : erreur serveur 5xx -->
<rule id="100703" level="8">
<if_sid>100701</if_sid>
<regex>| 5\d\d |</regex>
<description>API: Erreur serveur 5xx — $(srcip)</description>
<group>api_backend,api_http,api_error,</group>
</rule>
<!-- Erreur Go (panic, fatal, error) -->
<rule id="100704" level="9">
<if_sid>100700</if_sid>
<match>panic|PANIC|fatal|FATAL</match>
<description>API: Erreur critique Go backend (panic/fatal)</description>
<group>api_backend,api_error,</group>
</rule>
</group>
<!-- ── Auth failures via logs Go backend ────────────────────────────── -->
<group name="web,authentication_failures,api,">
<!-- Echec login admin (log Go direct) -->
<rule id="100600" level="5">
<decoded_as>go-backend-login-fail</decoded_as>
<match>LOGIN_ADMIN</match>
<description>API: Echec auth admin</description>
<group>api_auth_failure,</group>
</rule>
<!-- Echec login client (log Go direct) -->
<rule id="100601" level="5">
<decoded_as>go-backend-login-fail</decoded_as>
<match>LOGIN_CLIENT</match>
<description>API: Echec auth client</description>
<group>api_auth_failure,</group>
</rule>
<!-- Brute-force login admin : 5 echecs en 60s -->
<rule id="100610" level="10" frequency="5" timeframe="60">
<if_matched_sid>100600</if_matched_sid>
<same_source_ip />
<description>API: Brute-force login admin — 5 echecs/60s depuis $(srcip)</description>
<group>api_brute_force,authentication_failures,</group>
</rule>
<!-- Brute-force login client : 5 echecs en 60s -->
<rule id="100611" level="10" frequency="5" timeframe="60">
<if_matched_sid>100601</if_matched_sid>
<same_source_ip />
<description>API: Brute-force login client — 5 echecs/60s depuis $(srcip)</description>
<group>api_brute_force,authentication_failures,</group>
</rule>
<!-- Scan auth : 20 echecs en 120s toutes routes -->
<rule id="100612" level="12" frequency="20" timeframe="120">
<if_matched_group>api_auth_failure</if_matched_group>
<same_source_ip />
<description>API: Scan auth — 20 echecs/120s depuis $(srcip)</description>
<group>api_brute_force,authentication_failures,</group>
</rule>
</group>
@@ -0,0 +1,70 @@
<!-- Règles WireGuard VPN — IDs 100800-100820 -->
<group name="wireguard,vpn,">
<!-- Keepalive : niveau 0 = pas d'index dans OpenSearch -->
<rule id="100800" level="0">
<decoded_as>wireguard-keepalive</decoded_as>
<description>WireGuard: keepalive peer $(srcip)</description>
<group>wireguard,vpn_keepalive,</group>
</rule>
<!-- Handshake initiation (VPN server → peer) -->
<rule id="100801" level="3">
<decoded_as>wireguard-handshake-init</decoded_as>
<description>WireGuard: handshake initiation vers peer $(srcip)</description>
<group>wireguard,vpn_handshake,</group>
</rule>
<!-- Handshake réponse envoyée (VPN server → peer) -->
<rule id="100802" level="3">
<decoded_as>wireguard-handshake-resp-sent</decoded_as>
<description>WireGuard: handshake reponse envoyee a peer $(srcip)</description>
<group>wireguard,vpn_handshake,</group>
</rule>
<!-- Handshake réponse reçue (peer → VPN server) -->
<rule id="100803" level="3">
<decoded_as>wireguard-handshake-resp-recv</decoded_as>
<description>WireGuard: handshake complete avec peer $(srcip)</description>
<group>wireguard,vpn_handshake,</group>
</rule>
<!-- Timeout handshake (premier retry) -->
<rule id="100810" level="7">
<decoded_as>wireguard-timeout</decoded_as>
<description>WireGuard: handshake timeout peer $(srcip) (essai $(extra_data))</description>
<group>wireguard,vpn_warning,</group>
</rule>
<!-- Timeout répété — peer probablement déconnecté (try >= 5) -->
<rule id="100811" level="10">
<if_sid>100810</if_sid>
<match>try 5|try 6|try 7|try 8|try 9</match>
<description>WireGuard: peer $(srcip) deconnecte — handshake echoue 5 fois consecutives</description>
<group>wireguard,vpn_disconnected,</group>
</rule>
<!-- Timeout grave — try 10+ -->
<rule id="100812" level="12">
<if_sid>100810</if_sid>
<regex>retrying \(try 1\d\)</regex>
<description>WireGuard: peer $(srcip) hors ligne depuis plus de 60 secondes</description>
<group>wireguard,vpn_disconnected,</group>
</rule>
<!-- Peer roaming (changement d'IP publique) -->
<rule id="100815" level="5">
<decoded_as>wireguard-roaming</decoded_as>
<description>WireGuard: peer $(id) a change d'IP (roaming) vers $(srcip)</description>
<group>wireguard,vpn_roaming,</group>
</rule>
<!-- Peer reconnecté (handshake complet) -->
<rule id="100816" level="6">
<decoded_as>wireguard-handshake-resp-recv</decoded_as>
<description>WireGuard: peer $(srcip) handshake complet (reconnexion possible)</description>
<group>wireguard,vpn_reconnected,</group>
</rule>
</group>
@@ -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://<monitoring-ip>/#/app/dashboards#/view/{r['id']}")
else:
print(f"\n ERR Dashboard: {r}")
sys.exit(1)
print("\n=== Termine ===")
if __name__ == "__main__":
main()
@@ -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://<monitoring-ip>/#/app/dashboards#/view/{r['id']}")
else:
print(f"\n ERR Dashboard: {r}")
sys.exit(1)
print("\n=== Termine ===")
if __name__ == "__main__":
main()
@@ -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://<monitoring-ip>/#/app/dashboards#/view/{r['id']}")
else:
print(f"\n ERR Dashboard: {r}")
sys.exit(1)
print("\n=== Termine ===")
if __name__ == "__main__":
main()
@@ -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://<monitoring-ip>/#/app/dashboards#/view/{r['id']}")
else:
print(f"\n ❌ Dashboard: {r}")
sys.exit(1)
print("\n=== Terminé ===")
if __name__ == "__main__":
main()