chore
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+57
@@ -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()
|
||||
Reference in New Issue
Block a user