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
@@ -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>