chore: update

This commit is contained in:
Xor290
2026-08-08 12:04:57 +02:00
parent 56aef1fa9b
commit 8e36acd812
76 changed files with 12710 additions and 29 deletions
+10
View File
@@ -0,0 +1,10 @@
nodes:
indexer:
- name: wazuh.indexer
ip: "wazuh.indexer"
server:
- name: wazuh.manager
ip: "wazuh.manager"
dashboard:
- name: wazuh.dashboard
ip: "wazuh.dashboard"
@@ -0,0 +1,12 @@
server.host: 0.0.0.0
server.port: 5601
opensearch.hosts: https://wazuh.indexer:9200
opensearch.ssl.verificationMode: certificate
opensearch.ssl.certificateAuthorities: ["/usr/share/wazuh-dashboard/certs/root-ca.pem"]
opensearch.requestHeadersWhitelist: ["securitytenant","Authorization"]
opensearch_security.multitenancy.enabled: false
opensearch_security.readonly_mode.roles: ["kibana_read_only"]
server.ssl.enabled: true
server.ssl.key: "/usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem"
server.ssl.certificate: "/usr/share/wazuh-dashboard/certs/wazuh-dashboard.pem"
uiSettings.overrides.defaultRoute: /app/wz-home
@@ -0,0 +1,45 @@
---
_meta:
type: "internalusers"
config_version: 2
admin:
hash: "$2b$12$lNbkFg7B3IN.BXwZHjztteTDd3hTIvT/kTJhRFqfUY/228Bw45POO"
reserved: true
backend_roles:
- "admin"
description: "Admin user"
kibanaserver:
hash: "$2b$12$czi/zi7uht/K5.vcCvsXueFi6ot7GFwFfVBax7rCaNqBq1jhTOZ7u"
reserved: true
description: "Kibanaserver user"
kibanaro:
hash: "$2b$12$lNbkFg7B3IN.BXwZHjztteTDd3hTIvT/kTJhRFqfUY/228Bw45POO"
reserved: false
backend_roles:
- "kibanauser"
- "readall"
description: "Kibana read-only user"
logstash:
hash: "$2b$12$lNbkFg7B3IN.BXwZHjztteTDd3hTIvT/kTJhRFqfUY/228Bw45POO"
reserved: false
backend_roles:
- "logstash"
description: "Logstash user"
readall:
hash: "$2b$12$lNbkFg7B3IN.BXwZHjztteTDd3hTIvT/kTJhRFqfUY/228Bw45POO"
reserved: false
backend_roles:
- "readall"
description: "Readall user"
snapshotrestore:
hash: "$2b$12$lNbkFg7B3IN.BXwZHjztteTDd3hTIvT/kTJhRFqfUY/228Bw45POO"
reserved: false
backend_roles:
- "snapshotrestore"
description: "Snapshotrestore user"
@@ -0,0 +1,51 @@
network.host: "0.0.0.0"
node.name: "wazuh.indexer"
cluster.name: "wazuh-cluster"
# Compatibilité Filebeat 7.x (libbeat) avec OpenSearch 2.x
# Filebeat envoie _type dans les requêtes bulk — OpenSearch 2.x le rejette
# sans ce flag.
compatibility.override_main_response_version: true
path.data: /var/lib/wazuh-indexer
path.logs: /var/log/wazuh-indexer
plugins.security.ssl.http.pemcert_filepath: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.pem
plugins.security.ssl.http.pemkey_filepath: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.key
plugins.security.ssl.http.pemtrustedcas_filepath: /usr/share/wazuh-indexer/config/certs/root-ca.pem
plugins.security.ssl.transport.pemcert_filepath: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.pem
plugins.security.ssl.transport.pemkey_filepath: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.key
plugins.security.ssl.transport.pemtrustedcas_filepath: /usr/share/wazuh-indexer/config/certs/root-ca.pem
plugins.security.ssl.http.enabled: true
plugins.security.ssl.transport.enforce_hostname_verification: false
plugins.security.ssl.transport.resolve_hostname: false
plugins.security.authcz.admin_dn:
- "CN=admin,OU=Wazuh,O=Wazuh,L=California,C=US"
plugins.security.nodes_dn:
- "CN=wazuh.indexer,OU=Wazuh,O=Wazuh,L=California,C=US"
plugins.security.check_snapshot_restore_write_privileges: true
plugins.security.enable_snapshot_restore_privilege: true
plugins.security.restapi.roles_enabled:
- "all_access"
- "security_rest_api_access"
plugins.security.system_indices.enabled: true
plugins.security.system_indices.indices:
- ".opendistro-alerting-config"
- ".opendistro-alerting-alert*"
- ".opendistro-anomaly-results*"
- ".opendistro-anomaly-detector*"
- ".opendistro-anomaly-checkpoints"
- ".opendistro-anomaly-detection-state"
- ".opendistro-reports-*"
- ".opendistro-notifications-*"
- ".opendistro-notebooks"
- ".opensearch-observability"
- ".opendistro-asynchronous-search-response*"
- ".replication-metadata-store"
discovery.type: single-node
plugins.security.allow_default_init_securityindex: true
cluster.routing.allocation.disk.threshold_enabled: false
@@ -0,0 +1,63 @@
<!-- 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 ──────────────────────────────────────────────────── -->
<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>
<!-- ── 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>
<!-- ── Unattended-upgrades (patchs de sécurité auto) ───────────────── -->
<localfile>
<log_format>syslog</log_format>
<location>/var/log/unattended-upgrades/unattended-upgrades.log</location>
</localfile>
<!-- ── auditd (regles CIS 6.2.3.x deployees via ansible/hardening) ─── -->
<localfile>
<log_format>audit</log_format>
<location>/var/log/audit/audit.log</location>
</localfile>
</agent_config>
<!-- ── Logs backend Go (prod-mln) ────────────────────────────────── -->
<!-- "command" (pas full_command) = chaque ligne de docker logs → event séparé -->
<!-- Chaque ligne [GIN] devient son propre event → decoder peut extraire srcip/url/status -->
<agent_config name="prod-mln">
<localfile>
<log_format>command</log_format>
<command>docker logs --since 65s gestion-backend 2>&1</command>
<alias>backend</alias>
<frequency>60</frequency>
</localfile>
</agent_config>
<!-- ── Logs backend Go (pre-prod-mln) ───────────────────────────── -->
<agent_config name="pre-prod-mln">
<localfile>
<log_format>command</log_format>
<command>docker logs --since 65s gestion-backend 2>&1</command>
<alias>backend</alias>
<frequency>60</frequency>
</localfile>
</agent_config>
@@ -0,0 +1,4 @@
<!-- ModSecurity decoder — non utilisé, remplacé par règles json+location -->
<decoder name="waf-modsec-unused">
<prematch>UNUSED_MODSEC_PLACEHOLDER_NEVER_MATCHES</prematch>
</decoder>
@@ -0,0 +1,45 @@
<!-- WireGuard Decoders
Parent: kernel (built-in, program_name=^kernel)
Decoders enfants directs de kernel (Wazuh: 2 niveaux max) -->
<decoder name="wireguard-handshake-init">
<parent>kernel</parent>
<prematch>wireguard: wg0: Sending handshake initiation</prematch>
<regex type="pcre2">wireguard: wg0: Sending handshake initiation to peer (\d+) \(([\d.]+):\d+\)</regex>
<order>id,srcip</order>
</decoder>
<decoder name="wireguard-handshake-resp-sent">
<parent>kernel</parent>
<prematch>wireguard: wg0: Sending handshake response</prematch>
<regex type="pcre2">wireguard: wg0: Sending handshake response to peer (\d+) \(([\d.]+):\d+\)</regex>
<order>id,srcip</order>
</decoder>
<decoder name="wireguard-handshake-resp-recv">
<parent>kernel</parent>
<prematch>wireguard: wg0: Receiving handshake response</prematch>
<regex type="pcre2">wireguard: wg0: Receiving handshake response from peer (\d+) \(([\d.]+):\d+\)</regex>
<order>id,srcip</order>
</decoder>
<decoder name="wireguard-keepalive">
<parent>kernel</parent>
<prematch>wireguard: wg0: </prematch>
<regex type="pcre2">wireguard: wg0: \S+ keepalive packet \S+ peer (\d+) \(([\d.]+):\d+\)</regex>
<order>id,srcip</order>
</decoder>
<decoder name="wireguard-timeout">
<parent>kernel</parent>
<prematch>wireguard: wg0: Handshake for peer</prematch>
<regex type="pcre2">wireguard: wg0: Handshake for peer (\d+)[^(]+\(([\d.]+):\d+\).+retrying \(try (\d+)\)</regex>
<order>id,srcip,extra_data</order>
</decoder>
<decoder name="wireguard-roaming">
<parent>kernel</parent>
<prematch>wireguard: wg0: Peer </prematch>
<regex type="pcre2">wireguard: wg0: Peer (\d+).+roaming to ([\d.]+):\d+</regex>
<order>id,srcip</order>
</decoder>
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
set -e
# ─── Copie de la config custom (montée hors du volume wazuh_etc) ──────────────
CUSTOM=/wazuh-custom
cp "$CUSTOM/ossec.conf" /var/ossec/etc/ossec.conf
mkdir -p /var/ossec/etc/decoders /var/ossec/etc/rules /var/ossec/etc/lists
cp "$CUSTOM/decoders/"*.xml /var/ossec/etc/decoders/ 2>/dev/null || true
cp "$CUSTOM/rules/"*.xml /var/ossec/etc/rules/ 2>/dev/null || true
cp "$CUSTOM/lists/"* /var/ossec/etc/lists/ 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
@@ -0,0 +1,23 @@
<!-- ClamAV Log Monitoring -->
<!-- Collect ClamAV logs for antivirus detection -->
<!-- ClamAV Main Log -->
<localfile>
<log_format>syslog</log_format>
<location>/var/log/clamav/clamav.log</location>
<source_type>log</source_type>
</localfile>
<!-- ClamAV Freshclam (Signature Updates) Log -->
<localfile>
<log_format>syslog</log_format>
<location>/var/log/clamav/freshclam.log</location>
<source_type>log</source_type>
</localfile>
<!-- ClamAV Alert Log (High Priority Detections) -->
<localfile>
<log_format>syslog</log_format>
<location>/var/log/clamav/alert.log</location>
<source_type>log</source_type>
</localfile>
+960
View File
@@ -0,0 +1,960 @@
<ossec_config>
<!-- ═══════════════════════════════════════════════════════════
GLOBAL
═══════════════════════════════════════════════════════════ -->
<global>
<jsonout_output>yes</jsonout_output>
<alerts_log>yes</alerts_log>
<logall>no</logall>
<logall_json>no</logall_json>
<email_notification>no</email_notification>
<agents_disconnection_time>10m</agents_disconnection_time>
<agents_disconnection_alert_time>0</agents_disconnection_alert_time>
</global>
<!-- ═══════════════════════════════════════════════════════════
ALERTES
═══════════════════════════════════════════════════════════ -->
<alerts>
<log_alert_level>3</log_alert_level>
</alerts>
<!-- ═══════════════════════════════════════════════════════════
LOGGING
═══════════════════════════════════════════════════════════ -->
<logging>
<log_format>plain</log_format>
</logging>
<!-- ═══════════════════════════════════════════════════════════
COMMUNICATION AGENTS (port 1514 TCP)
═══════════════════════════════════════════════════════════ -->
<remote>
<connection>secure</connection>
<port>1514</port>
<protocol>tcp</protocol>
<queue_size>131072</queue_size>
</remote>
<!-- ═══════════════════════════════════════════════════════════
POLICY MONITORING
═══════════════════════════════════════════════════════════ -->
<rootcheck>
<disabled>no</disabled>
<check_files>yes</check_files>
<check_trojans>yes</check_trojans>
<check_dev>yes</check_dev>
<check_sys>yes</check_sys>
<check_pids>yes</check_pids>
<check_ports>yes</check_ports>
<check_if>yes</check_if>
<frequency>43200</frequency>
<rootkit_files>etc/rootcheck/rootkit_files.txt</rootkit_files>
<rootkit_trojans>etc/rootcheck/rootkit_trojans.txt</rootkit_trojans>
<skip_nfs>yes</skip_nfs>
</rootcheck>
<wodle name="cis-cat">
<disabled>yes</disabled>
<timeout>1800</timeout>
<interval>1d</interval>
<scan-on-start>yes</scan-on-start>
<java_path>wodles/java</java_path>
<ciscat_path>wodles/ciscat</ciscat_path>
</wodle>
<wodle name="osquery">
<disabled>yes</disabled>
<run_daemon>yes</run_daemon>
<log_path>/var/log/osquery/osqueryd.results.log</log_path>
<config_path>/etc/osquery/osquery.conf</config_path>
<add_labels>yes</add_labels>
</wodle>
<!-- ═══════════════════════════════════════════════════════════
SYSTEM INVENTORY
═══════════════════════════════════════════════════════════ -->
<wodle name="syscollector">
<disabled>no</disabled>
<interval>1h</interval>
<scan_on_start>yes</scan_on_start>
<hardware>yes</hardware>
<os>yes</os>
<network>yes</network>
<packages>yes</packages>
<ports all="yes">yes</ports>
<processes>yes</processes>
<synchronization>
<max_eps>10</max_eps>
</synchronization>
</wodle>
<!-- ═══════════════════════════════════════════════════════════
SECURITY CONFIGURATION ASSESSMENT
═══════════════════════════════════════════════════════════ -->
<sca>
<enabled>yes</enabled>
<scan_on_start>yes</scan_on_start>
<interval>12h</interval>
<skip_nfs>yes</skip_nfs>
</sca>
<!-- ═══════════════════════════════════════════════════════════
VULNERABILITY DETECTION
═══════════════════════════════════════════════════════════ -->
<vulnerability-detection>
<enabled>yes</enabled>
<index-status>yes</index-status>
<feed-update-interval>60m</feed-update-interval>
</vulnerability-detection>
<!-- ═══════════════════════════════════════════════════════════
INDEXER (Wazuh Indexer / OpenSearch)
═══════════════════════════════════════════════════════════ -->
<indexer>
<enabled>yes</enabled>
<hosts>
<host>https://wazuh.indexer:9200</host>
</hosts>
<ssl>
<certificate_authorities>
<ca>/etc/ssl/root-ca.pem</ca>
</certificate_authorities>
<certificate>/etc/ssl/filebeat.pem</certificate>
<key>/etc/ssl/filebeat.key</key>
</ssl>
</indexer>
<!-- ═══════════════════════════════════════════════════════════
FILE INTEGRITY MONITORING
═══════════════════════════════════════════════════════════ -->
<syscheck>
<disabled>no</disabled>
<frequency>43200</frequency>
<scan_on_start>yes</scan_on_start>
<alert_new_files>yes</alert_new_files>
<auto_ignore frequency="10" timeframe="3600">no</auto_ignore>
<directories>/etc,/usr/bin,/usr/sbin</directories>
<directories>/bin,/sbin,/boot</directories>
<ignore>/etc/mtab</ignore>
<ignore>/etc/hosts.deny</ignore>
<ignore>/etc/mail/statistics</ignore>
<ignore>/etc/random-seed</ignore>
<ignore>/etc/random.seed</ignore>
<ignore>/etc/adjtime</ignore>
<ignore>/etc/httpd/logs</ignore>
<ignore>/etc/utmpx</ignore>
<ignore>/etc/wtmpx</ignore>
<ignore>/etc/cups/certs</ignore>
<ignore>/etc/dumpdates</ignore>
<ignore>/etc/svc/volatile</ignore>
<ignore type="sregex">.log$|.swp$</ignore>
<nodiff>/etc/ssl/private.key</nodiff>
<skip_nfs>yes</skip_nfs>
<skip_dev>yes</skip_dev>
<skip_proc>yes</skip_proc>
<skip_sys>yes</skip_sys>
<process_priority>10</process_priority>
<max_eps>100</max_eps>
<synchronization>
<enabled>yes</enabled>
<interval>5m</interval>
<max_interval>1h</max_interval>
<max_eps>10</max_eps>
</synchronization>
</syscheck>
<!-- ═══════════════════════════════════════════════════════════
ACTIVE RESPONSE — WHITELIST
90.50.148.138 : IP publique admin — jamais bloquée
90.120.69.13 : IP publique admin — jamais bloquée
═══════════════════════════════════════════════════════════ -->
<global>
<white_list>127.0.0.1</white_list>
<white_list>^localhost.localdomain$</white_list>
<white_list>90.50.148.138</white_list>
<white_list>90.120.69.13</white_list>
<white_list>10.0.0.3</white_list>
</global>
<!-- ═══════════════════════════════════════════════════════════
ACTIVE RESPONSE — COMMANDES
═══════════════════════════════════════════════════════════ -->
<command>
<name>disable-account</name>
<executable>disable-account</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<command>
<name>restart-wazuh</name>
<executable>restart-wazuh</executable>
</command>
<command>
<name>firewall-drop</name>
<executable>firewall-drop</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<command>
<name>host-deny</name>
<executable>host-deny</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<command>
<name>route-null</name>
<executable>route-null</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<command>
<name>win_route-null</name>
<executable>route-null.exe</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<command>
<name>netsh</name>
<executable>netsh.exe</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<command>
<name>clamav-quarantine</name>
<executable>clamav-quarantine</executable>
<timeout_allowed>no</timeout_allowed>
</command>
<!-- ═══════════════════════════════════════════════════════════
ACTIVE RESPONSE — RÈGLES
═══════════════════════════════════════════════════════════ -->
<!-- SSH mauvais mot de passe → ban PERMANENT (1 tentative suffit) -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>5710,5711,5712,5716,5720,5760,5763</rules_id>
<timeout>0</timeout>
</active-response>
<!-- Brute-force SSH (plusieurs échecs) → ban PERMANENT -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_group>authentication_failures</rules_group>
<timeout>0</timeout>
</active-response>
<!-- Échec auth générique → ban PERMANENT -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_group>authentication_failed</rules_group>
<timeout>0</timeout>
</active-response>
<!-- SSH mauvais mot de passe → host-deny PERMANENT (double protection) -->
<active-response>
<command>host-deny</command>
<location>local</location>
<rules_id>5710,5711,5712,5716,5720,5760,5763</rules_id>
<timeout>0</timeout>
</active-response>
<!-- Attaques web / injection (niveau ≥ 7) → firewall-drop 5 min -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<level>7</level>
<rules_group>web,attack</rules_group>
<timeout>300</timeout>
</active-response>
<!-- Scan de ports → firewall-drop 1 heure -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>1002,2502,2503,2504</rules_id>
<timeout>3600</timeout>
</active-response>
<!-- SSH brute-force → host-deny PERMANENT (double protection) -->
<active-response>
<command>host-deny</command>
<location>local</location>
<rules_group>authentication_failures,authentication_failed</rules_group>
<timeout>0</timeout>
</active-response>
<!-- ModSecurity SQLi / XSS / LFI → firewall-drop 6 heures -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100202,100203,100205</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- ModSecurity RCE / anomalie critique → firewall-drop 24 heures -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100204,100206</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- ClamAV virus détecté → quarantaine du fichier infecté -->
<active-response>
<command>clamav-quarantine</command>
<location>local</location>
<rules_id>52502</rules_id>
</active-response>
<!-- Attaque soutenue ModSec (5 blocs / 2 min) → firewall-drop 24 heures -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100210</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- SSH brute-force (3 tentatives / 60s) → firewall-drop 1 heure -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100300</rules_id>
<timeout>3600</timeout>
</active-response>
<!-- SSH scan de comptes (3 users inexistants / 60s) → firewall-drop 24 heures -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100301</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- Sudo brute-force (3 échecs / 5 min) → disable-account 2h + firewall-drop 6h -->
<active-response>
<command>disable-account</command>
<location>local</location>
<rules_id>100310</rules_id>
<timeout>7200</timeout>
</active-response>
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100310</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- ═══════════════════════════════════════════════════════════
OWASP Top 10 — Active Response (ModSecurity)
═══════════════════════════════════════════════════════════ -->
<!-- OWASP#3 SQL Injection (100220) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100220</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#3 SQL Injection brute-force (100290) → firewall-drop 24h + host-deny -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100290</rules_id>
<timeout>86400</timeout>
</active-response>
<active-response>
<command>host-deny</command>
<location>local</location>
<rules_id>100290</rules_id>
<timeout>0</timeout>
</active-response>
<!-- OWASP#3 LDAP Injection (100221) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100221</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#3 Command Injection (100222) → firewall-drop 24h + host-deny permanent -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100222</rules_id>
<timeout>86400</timeout>
</active-response>
<active-response>
<command>host-deny</command>
<location>local</location>
<rules_id>100222</rules_id>
<timeout>0</timeout>
</active-response>
<!-- OWASP#3 Command Injection brute-force CRITICAL (100292) → permanent ban -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100292</rules_id>
<timeout>0</timeout>
</active-response>
<active-response>
<command>host-deny</command>
<location>local</location>
<rules_id>100292</rules_id>
<timeout>0</timeout>
</active-response>
<!-- OWASP#3 PHP/Java Injection (100223, 100224) → firewall-drop 12h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100223,100224</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP#6 RCE / Exploit attempts (100230) → firewall-drop 24h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100230</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP#7 XSS attacks (100240) → firewall-drop 4h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100240</rules_id>
<timeout>14400</timeout>
</active-response>
<!-- OWASP#7 XSS brute-force (100291) → firewall-drop 24h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100291</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP#7 HTTP Header Injection (100241) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100241</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#1 Path Traversal / LFI (100250) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100250</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#1 Path Traversal brute-force (100293) → firewall-drop 24h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100293</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP#1 Remote File Inclusion (100251) → firewall-drop 12h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100251</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP#1 Restricted File Access (100252) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100252</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#8 XXE (XML External Entity) (100260) → firewall-drop 12h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100260</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP#9 Scanner Detection (100270) → firewall-drop 2h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100270</rules_id>
<timeout>7200</timeout>
</active-response>
<!-- OWASP SSRF (100280) → firewall-drop 12h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100280</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP File Upload (100281) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100281</rules_id>
<timeout>21600</timeout>
</active-response>
<active-response>
<disabled>no</disabled>
<command>host-deny</command>
<location>all</location>
<rules_id>100210</rules_id>
<timeout>86400</timeout>
</active-response>
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100204,100206</rules_id>
<timeout>86400</timeout>
</active-response>
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>5710,5711,5712,5716,5720,5760,5763</rules_id>
<timeout>0</timeout>
</active-response>
<!-- Brute-force SSH (plusieurs échecs) → ban PERMANENT -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_group>authentication_failures</rules_group>
<timeout>0</timeout>
</active-response>
<!-- Échec auth générique → ban PERMANENT -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_group>authentication_failed</rules_group>
<timeout>0</timeout>
</active-response>
<!-- SSH mauvais mot de passe → host-deny PERMANENT (double protection) -->
<active-response>
<command>host-deny</command>
<location>all</location>
<rules_id>5710,5711,5712,5716,5720,5760,5763</rules_id>
<timeout>0</timeout>
</active-response>
<!-- Attaques web / injection (niveau ≥ 7) → firewall-drop 5 min -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<level>7</level>
<rules_group>web,attack</rules_group>
<timeout>300</timeout>
</active-response>
<!-- Scan de ports → firewall-drop 1 heure -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>1002,2502,2503,2504</rules_id>
<timeout>3600</timeout>
</active-response>
<!-- SSH brute-force → host-deny PERMANENT (double protection) -->
<active-response>
<command>host-deny</command>
<location>all</location>
<rules_group>authentication_failures,authentication_failed</rules_group>
<timeout>0</timeout>
</active-response>
<!-- ModSecurity SQLi / XSS / LFI → firewall-drop 6 heures -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100202,100203,100205</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- SSH brute-force (3 tentatives / 60s) → firewall-drop 1 heure sur tous les agents -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100300</rules_id>
<timeout>3600</timeout>
</active-response>
<!-- SSH scan de comptes → firewall-drop 24 heures sur tous les agents -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100301</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- ═══════════════════════════════════════════════════════════
OWASP Top 10 — Active Response (ModSecurity - location=all)
═══════════════════════════════════════════════════════════ -->
<!-- OWASP#3 SQLi (100220) → firewall-drop 6h (tous agents) -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100220</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#3 SQLi brute-force (100290) → firewall-drop 24h (tous agents) -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100290</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP#3 LDAP (100221) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100221</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#3 Command Injection (100222) → firewall-drop 24h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100222</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP#3 Command Injection CRITICAL (100292) → permanent ban -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100292</rules_id>
<timeout>0</timeout>
</active-response>
<!-- OWASP#3 PHP/Java (100223, 100224) → firewall-drop 12h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100223,100224</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP#6 RCE/Exploit (100230) → firewall-drop 24h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100230</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP#7 XSS (100240) → firewall-drop 4h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100240</rules_id>
<timeout>14400</timeout>
</active-response>
<!-- OWASP#7 XSS brute-force (100291) → firewall-drop 24h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100291</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP#7 Header Injection (100241) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100241</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#1 Path Traversal/LFI (100250) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100250</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#1 Path Traversal brute-force (100293) → firewall-drop 24h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100293</rules_id>
<timeout>86400</timeout>
</active-response>
<!-- OWASP#1 RFI (100251) → firewall-drop 12h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100251</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP#1 File Access (100252) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100252</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- OWASP#8 XXE (100260) → firewall-drop 12h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100260</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP#9 Scanner (100270) → firewall-drop 2h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100270</rules_id>
<timeout>7200</timeout>
</active-response>
<!-- OWASP SSRF (100280) → firewall-drop 12h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100280</rules_id>
<timeout>43200</timeout>
</active-response>
<!-- OWASP File Upload (100281) → firewall-drop 6h -->
<active-response>
<command>firewall-drop</command>
<location>all</location>
<rules_id>100281</rules_id>
<timeout>21600</timeout>
</active-response>
<!-- ═══════════════════════════════════════════════════════════
LOG ANALYSIS (commandes système)
═══════════════════════════════════════════════════════════ -->
<localfile>
<log_format>command</log_format>
<command>df -P</command>
<frequency>360</frequency>
</localfile>
<localfile>
<log_format>full_command</log_format>
<command>netstat -tulpn | sed 's/\([[:alnum:]]\+\)\ \+[[:digit:]]\+\ \+[[:digit:]]\+\ \+\(.*\):\([[:digit:]]*\)\ \+\([0-9\.\:\*]\+\).\+\ \([[:digit:]]*\/[[:alnum:]\-]*\).*/\1 \2 == \3 == \4 \5/' | sort -k 4 -g | sed 's/ == \(.*\) ==/:\1/' | sed 1,2d</command>
<alias>netstat listening ports</alias>
<frequency>360</frequency>
</localfile>
<localfile>
<log_format>full_command</log_format>
<command>last -n 20</command>
<frequency>360</frequency>
</localfile>
<!-- ═══════════════════════════════════════════════════════════
RULESET
═══════════════════════════════════════════════════════════ -->
<ruleset>
<!-- Règles et decoders natifs Wazuh -->
<decoder_dir>ruleset/decoders</decoder_dir>
<rule_dir>ruleset/rules</rule_dir>
<!-- Règles exclues — non pertinentes pour ce stack -->
<rule_exclude>0025-sendmail_rules.xml</rule_exclude>
<rule_exclude>0030-postfix_rules.xml</rule_exclude>
<rule_exclude>0035-spamd_rules.xml</rule_exclude>
<rule_exclude>0040-imapd_rules.xml</rule_exclude>
<rule_exclude>0045-mailscanner_rules.xml</rule_exclude>
<rule_exclude>0050-ms-exchange_rules.xml</rule_exclude>
<rule_exclude>0055-courier_rules.xml</rule_exclude>
<rule_exclude>0065-pix_rules.xml</rule_exclude>
<rule_exclude>0070-netscreenfw_rules.xml</rule_exclude>
<rule_exclude>0075-cisco-ios_rules.xml</rule_exclude>
<rule_exclude>0080-sonicwall_rules.xml</rule_exclude>
<rule_exclude>0090-telnetd_rules.xml</rule_exclude>
<rule_exclude>0100-solaris_bsm_rules.xml</rule_exclude>
<rule_exclude>0105-asterisk_rules.xml</rule_exclude>
<rule_exclude>0110-ms_dhcp_rules.xml</rule_exclude>
<rule_exclude>0115-arpwatch_rules.xml</rule_exclude>
<rule_exclude>0130-trend-osce_rules.xml</rule_exclude>
<rule_exclude>0135-hordeimp_rules.xml</rule_exclude>
<rule_exclude>0155-dovecot_rules.xml</rule_exclude>
<rule_exclude>0160-vmpop3d_rules.xml</rule_exclude>
<rule_exclude>0165-vpopmail_rules.xml</rule_exclude>
<rule_exclude>0170-ftpd_rules.xml</rule_exclude>
<rule_exclude>0175-proftpd_rules.xml</rule_exclude>
<rule_exclude>0185-vsftpd_rules.xml</rule_exclude>
<rule_exclude>0190-ms_ftpd_rules.xml</rule_exclude>
<rule_exclude>0195-named_rules.xml</rule_exclude>
<rule_exclude>0215-policy_rules.xml</rule_exclude>
<rule_exclude>0750-github_rules.xml</rule_exclude>
<rule_exclude>0250-apache_rules.xml</rule_exclude>
<rule_exclude>0255-zeus_rules.xml</rule_exclude>
<rule_exclude>0265-php_rules.xml</rule_exclude>
<rule_exclude>0275-squid_rules.xml</rule_exclude>
<rule_exclude>0295-mysql_rules.xml</rule_exclude>
<rule_exclude>0305-dropbear_rules.xml</rule_exclude>
<rule_exclude>0315-apparmor_rules.xml</rule_exclude>
<rule_exclude>0350-amazon_rules.xml</rule_exclude>
<rule_exclude>0360-serv-u_rules.xml</rule_exclude>
<rule_exclude>0385-oscap_rules.xml</rule_exclude>
<rule_exclude>0390-fortiddos_rules.xml</rule_exclude>
<rule_exclude>0391-fortigate_rules.xml</rule_exclude>
<rule_exclude>0392-fortimail_rules.xml</rule_exclude>
<rule_exclude>0393-fortiauth_rules.xml</rule_exclude>
<rule_exclude>0120-symantec-av_rules.xml</rule_exclude>
<rule_exclude>0125-symantec-ws_rules.xml</rule_exclude>
<rule_exclude>0395-hp_rules.xml</rule_exclude>
<rule_exclude>0405-rsa-auth-manager_rules.xml</rule_exclude>
<rule_exclude>0410-imperva_rules.xml</rule_exclude>
<rule_exclude>0415-sophos_rules.xml</rule_exclude>
<rule_exclude>0990-amazon-security-lake_rules.xml</rule_exclude>
<rule_exclude>0435-ms_logs_rules.xml</rule_exclude>
<rule_exclude>0445-identity_guard_rules.xml</rule_exclude>
<rule_exclude>0450-mongodb_rules.xml</rule_exclude>
<rule_exclude>0460-jenkins_rules.xml</rule_exclude>
<rule_exclude>0470-vshell_rules.xml</rule_exclude>
<rule_exclude>0475-suricata_rules.xml</rule_exclude>
<rule_exclude>0480-qualysguard_rules.xml</rule_exclude>
<rule_exclude>0485-cylance_rules.xml</rule_exclude>
<rule_exclude>0700-paloalto_rules.xml</rule_exclude>
<rule_exclude>0500-owncloud_rules.xml</rule_exclude>
<rule_exclude>0505-vuls_rules.xml</rule_exclude>
<rule_exclude>0525-openvas_rules.xml</rule_exclude>
<rule_exclude>0530-mysql_audit_rules.xml</rule_exclude>
<rule_exclude>0535-mariadb_rules.xml</rule_exclude>
<rule_exclude>0540-pfsense_rules.xml</rule_exclude>
<rule_exclude>0545-osquery_rules.xml</rule_exclude>
<rule_exclude>0550-kaspersky_rules.xml</rule_exclude>
<rule_exclude>0555-azure_rules.xml</rule_exclude>
<rule_exclude>0565-ms_ipsec_rules.xml</rule_exclude>
<rule_exclude>0575-win-base_rules.xml</rule_exclude>
<rule_exclude>0580-win-security_rules.xml</rule_exclude>
<rule_exclude>0585-win-application_rules.xml</rule_exclude>
<rule_exclude>0590-win-system_rules.xml</rule_exclude>
<rule_exclude>0601-win-vipre_rules.xml</rule_exclude>
<rule_exclude>0602-win-wfirewall_rules.xml</rule_exclude>
<rule_exclude>0610-win-ms_logs_rules.xml</rule_exclude>
<rule_exclude>0630-nextcloud_rules.xml</rule_exclude>
<rule_exclude>0675-panda-paps_rules.xml</rule_exclude>
<rule_exclude>0680-checkpoint-smart1_rules.xml</rule_exclude>
<rule_exclude>0770-gitlab_rules.xml</rule_exclude>
<rule_exclude>0775-arbor_rules.xml</rule_exclude>
<rule_exclude>0780-fireeye_rules.xml</rule_exclude>
<rule_exclude>0785-huawei-usg_rules.xml</rule_exclude>
<rule_exclude>0800-sysmon_id_1.xml</rule_exclude>
<rule_exclude>0810-sysmon_id_3.xml</rule_exclude>
<rule_exclude>0820-sysmon_id_7.xml</rule_exclude>
<rule_exclude>0830-sysmon_id_11.xml</rule_exclude>
<rule_exclude>0840-win_event_channel.xml</rule_exclude>
<rule_exclude>0860-sysmon_id_13.xml</rule_exclude>
<rule_exclude>0870-sysmon_id_8.xml</rule_exclude>
<rule_exclude>0905-cisco-ftd_rules.xml</rule_exclude>
<rule_exclude>0690-gcp_rules.xml</rule_exclude>
<rule_exclude>0910-ms-exchange-proxylogon_rules.xml</rule_exclude>
<rule_exclude>0920-oracledb_rules.xml</rule_exclude>
<rule_exclude>0925-eset-remote_rules.xml</rule_exclude>
<rule_exclude>0955-WEF-baseline_rules.xml</rule_exclude>
<rule_exclude>0960-macos_rules.xml</rule_exclude>
<rule_exclude>0995-microsoft-graph_rules.xml</rule_exclude>
<rule_exclude>0755-office365_rules.xml</rule_exclude>
<rule_exclude>0140-roundcube_rules.xml</rule_exclude>
<rule_exclude>0998-aws-security-hub-rules.xml</rule_exclude>
<rule_exclude>0400-openvpn_rules.xml</rule_exclude>
<list>etc/lists/audit-keys</list>
<list>etc/lists/amazon/aws-eventnames</list>
<list>etc/lists/security-eventchannel</list>
<list>etc/lists/malicious-ioc/malicious-ip</list>
<list>etc/lists/malicious-ioc/malicious-domains</list>
<list>etc/lists/malicious-ioc/malware-hashes</list>
<list>etc/lists/wireguard-trusted-ips</list>
<!-- Decoders et règles custom ModSecurity -->
<decoder_dir>etc/decoders</decoder_dir>
<rule_dir>etc/rules</rule_dir>
</ruleset>
<!-- ═══════════════════════════════════════════════════════════
RULE TEST
═══════════════════════════════════════════════════════════ -->
<rule_test>
<enabled>yes</enabled>
<threads>1</threads>
<max_sessions>64</max_sessions>
<session_timeout>15m</session_timeout>
</rule_test>
<!-- ═══════════════════════════════════════════════════════════
AUTHENTIFICATION AGENTS (wazuh-authd)
═══════════════════════════════════════════════════════════ -->
<auth>
<disabled>no</disabled>
<port>1515</port>
<use_source_ip>no</use_source_ip>
<purge>yes</purge>
<use_password>no</use_password>
<ciphers>HIGH:!ADH:!EXP:!MD5:!RC4:!3DES:!CAMELLIA:@STRENGTH</ciphers>
<ssl_verify_host>no</ssl_verify_host>
<ssl_manager_cert>etc/sslmanager.cert</ssl_manager_cert>
<ssl_manager_key>etc/sslmanager.key</ssl_manager_key>
<ssl_auto_negotiate>no</ssl_auto_negotiate>
</auth>
<!-- ═══════════════════════════════════════════════════════════
CLUSTER (standalone — désactivé)
═══════════════════════════════════════════════════════════ -->
<cluster>
<name>wazuh</name>
<node_name>node01</node_name>
<node_type>master</node_type>
<key>aa093264ef885029653eea20dfcf51ae</key>
<port>1516</port>
<bind_addr>0.0.0.0</bind_addr>
<nodes>
<node>wazuh.manager</node>
</nodes>
<hidden>no</hidden>
<disabled>yes</disabled>
</cluster>
</ossec_config>
<ossec_config>
<localfile>
<log_format>syslog</log_format>
<location>/var/ossec/logs/active-responses.log</location>
</localfile>
</ossec_config>
@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<group name="clamav">
<rule id="100500" level="0">
<decoded_as>syslog</decoded_as>
<program_name>clam</program_name>
<description>ClamAV message grouping</description>
</rule>
<!-- ClamAV Malware Detections (Critical) -->
<rule id="100501" level="15">
<parent>100500</parent>
<regex>FOUND|Infected|detected</regex>
<description>ClamAV: Malware detected - $(var malware_name)</description>
<group>malware,clamav,infection</group>
</rule>
<!-- ClamAV Trojan/PUA Detection -->
<rule id="100502" level="12">
<parent>100500</parent>
<regex>Trojan|PUA|unwanted|grayware</regex>
<description>ClamAV: Potentially Unwanted Application detected</description>
<group>malware,clamav,pua</group>
</rule>
<!-- ClamAV Virus Detection -->
<rule id="100503" level="15">
<parent>100500</parent>
<regex>\.Virus\.|\.Win\.|\.Linux\.|\.Worm\.|\.Backdoor</regex>
<description>ClamAV: Virus detected - High severity</description>
<group>malware,clamav,virus</group>
</rule>
<!-- ClamAV Ransomware Detection -->
<rule id="100504" level="15">
<parent>100500</parent>
<regex>Ransomware|Encrypted|Cryptolocker|BadRabbit|WannaCry</regex>
<description>ClamAV: Ransomware detected</description>
<group>malware,clamav,ransomware</group>
</rule>
<!-- ClamAV Signature Update Success -->
<rule id="100505" level="3">
<parent>100500</parent>
<program_name>freshclam</program_name>
<regex>ClamAV update completed successfully|updated</regex>
<description>ClamAV: Signature database updated successfully</description>
<group>clamav,updates</group>
</rule>
<!-- ClamAV Signature Update Failed -->
<rule id="100506" level="10">
<parent>100500</parent>
<program_name>freshclam</program_name>
<regex>ERROR|Failed|error downloading|connection failed|timeout</regex>
<description>ClamAV: Signature update failed</description>
<group>clamav,updates,error</group>
</rule>
<!-- ClamAV Daemon Error -->
<rule id="100507" level="8">
<parent>100500</parent>
<program_name>clamd</program_name>
<regex>ERROR|error|CRITICAL</regex>
<description>ClamAV: Daemon error</description>
<group>clamav,error</group>
</rule>
<!-- ClamAV Scanner Started -->
<rule id="100508" level="3">
<parent>100500</parent>
<regex>Scanning started|initializing scanner</regex>
<description>ClamAV: Scanning started</description>
<group>clamav,scanning</group>
</rule>
<!-- ClamAV Scanner Completed -->
<rule id="100509" level="3">
<parent>100500</parent>
<regex>Scanning finished|scan completed</regex>
<description>ClamAV: Scanning completed</description>
<group>clamav,scanning</group>
</rule>
<!-- ClamAV Corrupted Files Alert -->
<rule id="100510" level="8">
<parent>100500</parent>
<regex>corrupted|corrupt file|damaged|unreadable</regex>
<description>ClamAV: Corrupted file detected</description>
<group>clamav,corruption</group>
</rule>
<!-- ClamAV Quarantine Actions -->
<rule id="100511" level="10">
<parent>100500</parent>
<regex>quarantine|moved|removed|deleted|archived</regex>
<description>ClamAV: File quarantined/removed</description>
<group>clamav,quarantine,action</group>
</rule>
<!-- ClamAV Scan Statistics -->
<rule id="100512" level="3">
<parent>100500</parent>
<regex>infects|infected files|detected</regex>
<description>ClamAV: Scan statistics</description>
<group>clamav,statistics</group>
</rule>
<!-- ClamAV Permission Denied -->
<rule id="100513" level="6">
<parent>100500</parent>
<regex>Permission denied|access denied|unable to scan</regex>
<description>ClamAV: Permission denied when scanning file</description>
<group>clamav,access</group>
</rule>
<!-- ClamAV Database Problem -->
<rule id="100514" level="10">
<parent>100500</parent>
<regex>database error|bad database|corrupt database|outdated database</regex>
<description>ClamAV: Database problem detected</description>
<group>clamav,database,error</group>
</rule>
<!-- Aggregate: Multiple Malware Detections in Short Time -->
<rule id="100515" level="16">
<parent>100501</parent>
<frequency>3</timeframe>3600</frequency>
<same_source_ip />
<description>ClamAV: Multiple malware detections in short time - possible outbreak</description>
<group>malware,clamav,outbreak</group>
</rule>
<!-- Aggregate: Ransomware Alert with Infection -->
<rule id="100516" level="16">
<if_matched_sid>100501</if_matched_sid>
<if_matched_sid>100504</if_matched_sid>
<same_source_ip />
<timeframe>300</timeframe>
<description>ClamAV: Ransomware and virus detected together - critical threat</description>
<group>malware,clamav,ransomware,critical</group>
</rule>
</group>
@@ -0,0 +1,45 @@
<!-- ═══════════════════════════════════════════════════════════════════
Active-Response — règles de détection précoce pour l'AR graduée
═══════════════════════════════════════════════════════════════════
Ces règles viennent EN PLUS des bans existants sur les règles natives
(5710, 5716, etc.) pour produire des alertes clairement labellisées
et déclencher des AR spécifiques par scénario d'attaque.
-->
<!-- ─── SSH ─────────────────────────────────────────────────────────── -->
<group name="syslog,sshd,authentication_failures,">
<!-- 3 mots de passe incorrects en 60s depuis la même IP
Note : le ban PERMANENT sur 5716 (1ère tentative) reste actif en parallèle.
Cette règle produit une alerte "brute-force" explicite + AR temporaire graduée. -->
<rule id="100300" level="10" frequency="3" timeframe="60">
<if_matched_sid>5716</if_matched_sid>
<same_source_ip />
<description>SSH: brute-force — 3 mots de passe faux en 60s depuis $(srcip)</description>
<group>authentication_failures,ssh_brute,</group>
</rule>
<!-- 3 users inexistants en 60s depuis la même IP → scan de comptes -->
<rule id="100301" level="12" frequency="3" timeframe="60">
<if_matched_sid>5710</if_matched_sid>
<same_source_ip />
<description>SSH: scan de comptes — 3 users inexistants en 60s depuis $(srcip)</description>
<group>authentication_failures,ssh_scan,</group>
</rule>
</group>
<!-- ─── SUDO ─────────────────────────────────────────────────────────── -->
<group name="syslog,sudo,authentication_failures,">
<!-- 3 échecs sudo en 5 min par le même utilisateur.
Couverture non présente dans les règles natives → AR disable-account. -->
<rule id="100310" level="10" frequency="3" timeframe="300">
<if_matched_sid>5401</if_matched_sid>
<same_user />
<description>Sudo: 3 échecs d'auth en 5 min — tentative d'escalade de privilèges</description>
<group>authentication_failures,sudo_brute,</group>
</rule>
</group>
@@ -0,0 +1,105 @@
<!-- ═══════════════════════════════════════════════════════════════════
Règles d'audit CIS 6.2.3.x — IDs 100900-100930
Regles deployees par ansible/hardening/playbook-audit-rules-*.yml
(surveillance sudo, surveillance omnex, et regles CIS restantes).
Objectif : ne faire remonter dans le dashboard QUE les evenements
qui indiquent un probleme de securite potentiel (level >= 4, seuil
de <log_alert_level> dans ossec.conf). Le reste (chaque commande
sudo, chaque commande omnex, chmod/chown routiniers, montages
Docker, deletions de fichiers, sessions login normales) continue
d'etre capture dans /var/log/audit/audit.log sur chaque host
(consultable via `ausearch -k <cle>`) mais reste sous le seuil
d'alerte generique (level 3, rule 80780+) donc invisible du
dashboard — c'est le comportement voulu, pas un oubli.
Cles VOLONTAIREMENT laissees au niveau generique (pas de regle ici) :
user_emulation, omnex_actions, perm_mod, mounts, session, logins,
delete — activite routiniere d'administration, pas un signal de
securite en soi. Consultable via ausearch si besoin d'investiguer.
-->
<group name="audit,">
<!-- 6.2.3.1 — Modification de /etc/sudoers ou /etc/sudoers.d
Signal fort : quelqu'un modifie qui a le droit d'utiliser sudo. -->
<rule id="100900" level="12">
<if_sid>80700</if_sid>
<field name="audit.key">^scope$</field>
<description>Audit: /etc/sudoers modifie — changement de perimetre administrateur</description>
<group>audit_security,gdpr_IV_35.7.d,</group>
</rule>
<!-- 6.2.3.3 — Modification du fichier de log sudo (technique anti-forensique) -->
<rule id="100901" level="12">
<if_sid>80700</if_sid>
<field name="audit.key">^sudo_log_file$</field>
<description>Audit: /var/log/sudo.log modifie — possible tentative d'effacement de traces</description>
<group>audit_security,gdpr_IV_35.7.d,</group>
</rule>
<!-- 6.2.3.8 — Modification des fichiers d'identite (passwd/shadow/group/pam) -->
<rule id="100902" level="10">
<if_sid>80700</if_sid>
<field name="audit.key">^identity$</field>
<description>Audit: fichier d'identite systeme modifie (passwd/shadow/group/pam)</description>
<group>audit_security,gdpr_IV_35.7.d,</group>
</rule>
<!-- 6.2.3.4 — Changement de date/heure systeme (technique anti-forensique classique) -->
<rule id="100903" level="8">
<if_sid>80700</if_sid>
<field name="audit.key">^time-change$</field>
<description>Audit: horloge systeme modifiee</description>
<group>audit_security,</group>
</rule>
<!-- 6.2.3.5 — Changement d'environnement reseau (hostname, /etc/hosts, netplan...) -->
<rule id="100904" level="8">
<if_sid>80700</if_sid>
<field name="audit.key">^system-locale$</field>
<description>Audit: configuration reseau systeme modifiee (hostname/hosts/netplan)</description>
<group>audit_security,</group>
</rule>
<!-- 6.2.3.14 — Modification de la politique AppArmor (desactivation possible d'un control de securite) -->
<rule id="100905" level="10">
<if_sid>80700</if_sid>
<field name="audit.key">^MAC-policy$</field>
<description>Audit: politique AppArmor modifiee — possible desactivation d'un controle de securite</description>
<group>audit_security,</group>
</rule>
<!-- 6.2.3.15-17 — Usage de chcon/setfacl/chacl (commandes rares, manipulation de contexte/ACL) -->
<rule id="100906" level="8">
<if_sid>80700</if_sid>
<field name="audit.key">^perm_chng$</field>
<description>Audit: commande chcon/setfacl/chacl executee — manipulation de contexte ou d'ACL</description>
<group>audit_security,</group>
</rule>
<!-- 6.2.3.18 — Usage de usermod (modification de compte via commande, possible escalade) -->
<rule id="100907" level="8">
<if_sid>80700</if_sid>
<field name="audit.key">^usermod$</field>
<description>Audit: commande usermod executee — modification de compte utilisateur</description>
<group>audit_security,</group>
</rule>
<!-- 6.2.3.19 — Chargement/dechargement de module noyau (technique rootkit classique) -->
<rule id="100908" level="12">
<if_sid>80700</if_sid>
<field name="audit.key">^kernel_modules$</field>
<description>Audit: module noyau charge/decharge — signal potentiel de rootkit</description>
<group>audit_security,</group>
</rule>
<!-- 6.2.3.7 — Tentative d'acces fichier refusee (EACCES/EPERM) : quelqu'un a essaye et echoue -->
<rule id="100909" level="6">
<if_sid>80700</if_sid>
<field name="audit.key">^access$</field>
<description>Audit: tentative d'acces fichier refusee (permissions insuffisantes)</description>
<group>audit_security,</group>
</rule>
</group>
@@ -0,0 +1,39 @@
<!-- ═══════════════════════════════════════════════════════════════
Règles ModSecurity — WAF container (gestion-waf)
Le decoder json intégré parse les events — on utilise <field> + <location>.
═══════════════════════════════════════════════════════════════ -->
<group name="web,modsecurity,">
<!-- Base : tout event JSON provenant du log ModSecurity -->
<rule id="100100" level="3">
<decoded_as>json</decoded_as>
<location>modsec_audit.log</location>
<field name="transaction.client_ip">\.+</field>
<description>ModSecurity: WAF event capturé</description>
<group>web,modsecurity,</group>
</rule>
<!-- Accès bloqué (HTTP 403) -->
<rule id="100102" level="10">
<if_sid>100100</if_sid>
<field name="transaction.response.http_code">^403$</field>
<description>ModSecurity: accès bloqué (403) — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>web,modsecurity,attack,blocked,</group>
</rule>
<!-- Trafic passant avec règle déclenchée (warning, anomaly scoring) -->
<rule id="100101" level="6">
<if_sid>100100</if_sid>
<description>ModSecurity: règle WAF déclenchée (non bloqué) — $(transaction.client_ip)</description>
<group>web,modsecurity,attack,</group>
</rule>
<!-- Blocages répétés depuis la même IP -->
<rule id="100103" level="12" frequency="5" timeframe="60">
<if_matched_sid>100102</if_matched_sid>
<description>ModSecurity: blocages répétés — possible scan ou attaque soutenue</description>
<group>web,modsecurity,attack,blocked,</group>
</rule>
</group>
@@ -0,0 +1,113 @@
<!-- ═══════════════════════════════════════════════════════════════════
Suppression des alertes SCA (Security Configuration Assessment)
═══════════════════════════════════════════════════════════════════
Les scans CIS benchmark génèrent des centaines d'alertes par cycle.
Toutes passées à level 0 — les résultats restent consultables dans
l'onglet SCA du dashboard sans polluer le flux d'alertes.
-->
<group name="sca,">
<!-- ─── Résumés de scan (score < 90%, <80%, <50%, <30%) ─────────── -->
<rule id="19001" level="0" overwrite="yes">
<if_sid>19000</if_sid>
<field name="sca.type">^summary</field>
<options>no_full_log</options>
<description>SCA summary — supprimé (voir onglet SCA du dashboard).</description>
</rule>
<rule id="19002" level="0" overwrite="yes">
<if_sid>19001</if_sid>
<field name="sca.score">^8\d</field>
<options>no_full_log</options>
<description>SCA summary: score &lt; 90% — supprimé.</description>
</rule>
<rule id="19003" level="0" overwrite="yes">
<if_sid>19001</if_sid>
<field name="sca.score">^7\d|^6\d|^5\d</field>
<options>no_full_log</options>
<description>SCA summary: score &lt; 80% — supprimé.</description>
</rule>
<rule id="19004" level="0" overwrite="yes">
<if_sid>19001</if_sid>
<field name="sca.score">^4\d|^3\d</field>
<options>no_full_log</options>
<description>SCA summary: score &lt; 50% — supprimé.</description>
</rule>
<rule id="19005" level="0" overwrite="yes">
<if_sid>19001</if_sid>
<field name="sca.score">^2\d|^1\d$|^\d$</field>
<options>no_full_log</options>
<description>SCA summary: score &lt; 30% — supprimé.</description>
</rule>
<!-- ─── Checks individuels (passed / failed / not applicable) ────── -->
<rule id="19007" level="0" overwrite="yes">
<if_sid>19006</if_sid>
<field name="sca.check.result">^failed</field>
<options>no_full_log</options>
<description>SCA check failed — supprimé.</description>
</rule>
<rule id="19008" level="0" overwrite="yes">
<if_sid>19006</if_sid>
<field name="sca.check.result">^passed</field>
<options>no_full_log</options>
<description>SCA check passed — supprimé.</description>
</rule>
<rule id="19009" level="0" overwrite="yes">
<if_sid>19006</if_sid>
<field name="sca.check.result">^not applicable</field>
<options>no_full_log</options>
<description>SCA check not applicable — supprimé.</description>
</rule>
<!-- ─── Changements de statut entre scans ───────────────────────── -->
<rule id="19010" level="0" overwrite="yes">
<if_sid>19008</if_sid>
<field name="sca.check.previous_result">^failed</field>
<options>no_full_log</options>
<description>SCA: failed → passed — supprimé.</description>
</rule>
<rule id="19011" level="0" overwrite="yes">
<if_sid>19007</if_sid>
<field name="sca.check.previous_result">^passed</field>
<options>no_full_log</options>
<description>SCA: passed → failed — supprimé.</description>
</rule>
<rule id="19012" level="0" overwrite="yes">
<if_sid>19009</if_sid>
<field name="sca.check.previous_result">^passed</field>
<options>no_full_log</options>
<description>SCA: passed → not applicable — supprimé.</description>
</rule>
<rule id="19013" level="0" overwrite="yes">
<if_sid>19009</if_sid>
<field name="sca.check.previous_result">^failed</field>
<options>no_full_log</options>
<description>SCA: failed → not applicable — supprimé.</description>
</rule>
<rule id="19014" level="0" overwrite="yes">
<if_sid>19007</if_sid>
<field name="sca.check.previous_result">^not applicable</field>
<options>no_full_log</options>
<description>SCA: not applicable → failed — supprimé.</description>
</rule>
<rule id="19015" level="0" overwrite="yes">
<if_sid>19008</if_sid>
<field name="sca.check.previous_result">^not applicable</field>
<options>no_full_log</options>
<description>SCA: not applicable → passed — supprimé.</description>
</rule>
</group>
@@ -0,0 +1,133 @@
<!-- ═══════════════════════════════════════════════════════════════════
Réduction du bruit SSH/PAM — overrides des règles natives Wazuh
═══════════════════════════════════════════════════════════════════
Supprimés (level 0) : événements sans valeur sécurité générés en masse
Conservés intacts : tout ce qui alimente l'active-response et la
détection de brute-force (5710, 5716, 5720, 5760, 5763…)
-->
<!-- ─── DPKG ───────────────────────────────────────────────────────────── -->
<group name="syslog,dpkg,">
<rule id="2901" level="0" overwrite="yes">
<if_sid>2900</if_sid>
<description>Dpkg: supprimé.</description>
</rule>
<rule id="2902" level="0" overwrite="yes">
<if_sid>2900</if_sid>
<description>Dpkg: supprimé.</description>
</rule>
<rule id="2903" level="0" overwrite="yes">
<if_sid>2900</if_sid>
<description>Dpkg: supprimé.</description>
</rule>
<rule id="2904" level="0" overwrite="yes">
<if_sid>2900</if_sid>
<description>Dpkg: supprimé.</description>
</rule>
</group>
<!-- ─── IP ADMIN (90.50.148.138) ─────────────────────────────────────── -->
<!-- Auth success supprimé pour l'IP admin : pas de valeur sécurité,
les échecs restent visibles si l'IP était usurpée. -->
<group name="syslog,sshd,">
<rule id="100010" level="0">
<if_sid>5715</if_sid>
<srcip>90.50.148.138</srcip>
<description>sshd: auth success IP admin — supprimé.</description>
</rule>
</group>
<!-- ─── PAM ──────────────────────────────────────────────────────────── -->
<group name="pam,syslog,">
<!-- 5501 (level 3) → PAM "session opened" : redondant avec 5715 sshd auth success -->
<rule id="5501" level="0" overwrite="yes">
<if_sid>5500</if_sid>
<match>session opened for user </match>
<description>PAM: Login session opened (bruit supprimé).</description>
</rule>
<!-- 5502 (level 3) → PAM "session closed" : purement informatif -->
<rule id="5502" level="0" overwrite="yes">
<if_sid>5500</if_sid>
<match>session closed for user </match>
<description>PAM: Login session closed (bruit supprimé).</description>
</rule>
</group>
<!-- ─── SSH ──────────────────────────────────────────────────────────── -->
<group name="syslog,sshd,">
<!-- 5704 (level 4) → timeout individuel : la règle de fréquence 5705
(level 10) continue de compter et d'alerter sur les patterns -->
<rule id="5704" level="0" overwrite="yes">
<if_sid>5700</if_sid>
<match>fatal: Timeout before authentication for</match>
<description>sshd: Timeout individuel supprimé (fréquence 5705 active).</description>
</rule>
<!-- 5706 (level 6) → "Did not receive identification string" : scanners
qui touchent le port 22 sans finir le handshake, très fréquent -->
<rule id="5706" level="0" overwrite="yes">
<if_sid>5700</if_sid>
<match>Did not receive identification string from</match>
<description>sshd: Sonde sans identification (bruit de scan supprimé).</description>
</rule>
<!-- 5740 (level 4) → connection reset by peer : bruit réseau/bots -->
<rule id="5740" level="0" overwrite="yes">
<if_sid>5700</if_sid>
<match>Connection reset by peer$</match>
<description>sshd: Connection reset by peer (bruit supprimé).</description>
</rule>
<!-- 5741 (level 4) → connection refused : pas d'intérêt sur un agent -->
<rule id="5741" level="0" overwrite="yes">
<if_sid>5700</if_sid>
<match>Connection refused$</match>
<description>sshd: Connection refused (bruit supprimé).</description>
</rule>
<!-- 5749 (level 4) → bad packet length : scanners/bots -->
<rule id="5749" level="0" overwrite="yes">
<if_sid>5700</if_sid>
<match>^Bad packet length</match>
<description>sshd: Bad packet length (bruit de scan supprimé).</description>
</rule>
<!-- 5762 (level 4) → connection reset : doublon de 5740 -->
<rule id="5762" level="0" overwrite="yes">
<if_sid>5700</if_sid>
<match>Connection reset</match>
<description>sshd: Connection reset (bruit supprimé).</description>
</rule>
</group>
<!-- ─── IOC Suricata/Windows — parents exclus (0475, 0580, 0840) ──────────
IMPORTANT: <match>PLACEHOLDER_NEVER_MATCHES</match> au lieu de <match>.</match>
pour éviter que ces rules catch-all bloquent l'évaluation des rules avec
un ID plus élevé (first-match semantics dans Wazuh). -->
<group name="ids,">
<rule id="99917" level="0" overwrite="yes">
<match>WAZUH_PLACEHOLDER_SURICATA_NEVER_MATCHES</match>
<description>Suricata: malicious domain — overwrite placeholder (non applicable).</description>
</rule>
<rule id="99918" level="0" overwrite="yes">
<match>WAZUH_PLACEHOLDER_SURICATA_DNS_NEVER_MATCHES</match>
<description>Suricata: malicious domain DNS — overwrite placeholder (non applicable).</description>
</rule>
</group>
<group name="windows,windows_security,">
<rule id="99919" level="0" overwrite="yes">
<match>WAZUH_PLACEHOLDER_WINDOWS_FAILED_NEVER_MATCHES</match>
<description>Windows: failed logon malicious IP — overwrite placeholder (non applicable).</description>
</rule>
<rule id="99920" level="0" overwrite="yes">
<match>WAZUH_PLACEHOLDER_WINDOWS_SUCCESS_NEVER_MATCHES</match>
<description>Windows: successful logon malicious IP — overwrite placeholder (non applicable).</description>
</rule>
</group>
@@ -0,0 +1,352 @@
<!-- ═══════════════════════════════════════════════════════════════════
ModSecurity OWASP Top 10 — Règles granulaires
═══════════════════════════════════════════════════════════════════
Détection des attaques OWASP Top 10 via ModSecurity + active-response
Correspondances CRS (OWASP ModSec Core Rule Set) :
• 941xxx : XSS (Cross-Site Scripting)
• 942xxx : SQL Injection, LDAP Injection
• 943xxx : XXE (XML External Entity)
• 930xxx : Path Traversal, RFI (Remote File Inclusion)
• 932xxx : Remote Command Execution
• 933xxx : PHP Injection
• 934xxx : Java Injection
• 950xxx : Exploit attempts
• 951xxx : Regex DoS
• 952xxx : Restricted File Access
• 953xxx : Insecure File Upload
• 954xxx : Proxy Abuse
• 955xxx : Malicious HTTP Headers
• 970xxx : SSRF / Open Redirect
-->
<group name="web,modsecurity,owasp,">
<!-- ═══════════════════════════════════════════════════════════
RÈGLES BLOQUÉES (100102 - HTTP 403)
═══════════════════════════════════════════════════════════ -->
<!-- ═══════════════════════════════════════════════════════════
OWASP #3 : INJECTION (SQLi, Command Injection, LDAP, etc.)
═══════════════════════════════════════════════════════════ -->
<!-- SQL Injection BLOQUÉE (942xx) -->
<rule id="100220" level="13">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">942[0-9]</field>
<description>OWASP#3 SQL Injection bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_injection,sql_injection,</group>
</rule>
<!-- LDAP Injection (942xx - LDAP patterns) -->
<rule id="100221" level="12">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">942[0-9].*LDAP\|ldap_filter</field>
<description>OWASP#3 LDAP Injection bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_injection,ldap_injection,</group>
</rule>
<!-- Command Injection / OS Command Execution (932xx) -->
<rule id="100222" level="14">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">932[0-9]</field>
<description>OWASP#3 Remote Command Injection bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_injection,command_injection,rce,</group>
</rule>
<!-- PHP Injection (933xx) -->
<rule id="100223" level="13">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">933[0-9]</field>
<description>OWASP#3 PHP Injection bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_injection,php_injection,</group>
</rule>
<!-- Java Injection (934xx) -->
<rule id="100224" level="13">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">934[0-9]</field>
<description>OWASP#3 Java Injection bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_injection,java_injection,</group>
</rule>
<!-- ═══════════════════════════════════════════════════════════
OWASP #6 : VULNERABLE & OUTDATED COMPONENTS (Exploit attempts)
═══════════════════════════════════════════════════════════ -->
<!-- Remote Code Execution attempts (930xx, 950xx) -->
<rule id="100230" level="14">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">930[0-9]\|950[0-9]</field>
<description>OWASP#6 RCE / Exploit attempt bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_vulnerable_components,rce,exploit,</group>
</rule>
<!-- ═══════════════════════════════════════════════════════════
OWASP #7 : AUTHENTICATION & SESSION (XSS/CSRF/Session Theft)
═══════════════════════════════════════════════════════════ -->
<!-- Cross-Site Scripting (941xx) -->
<rule id="100240" level="12">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">941[0-9]</field>
<description>OWASP#7 XSS attack bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_auth,xss,session_theft,</group>
</rule>
<!-- HTTP Response Splitting / Header Injection (955xx) -->
<rule id="100241" level="12">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">955[0-9]</field>
<description>OWASP#7 HTTP Header Injection bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_auth,header_injection,</group>
</rule>
<!-- ═══════════════════════════════════════════════════════════
OWASP #1 : BROKEN ACCESS CONTROL (Path Traversal, Bypass)
═══════════════════════════════════════════════════════════ -->
<!-- Path Traversal / Local File Inclusion (930xx - LFI patterns) -->
<rule id="100250" level="12">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">930[0-9].*\(path\|traversal\|lfi\)</field>
<description>OWASP#1 Path Traversal / LFI bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_access_control,lfi,path_traversal,</group>
</rule>
<!-- Remote File Inclusion (930xx - RFI patterns) -->
<rule id="100251" level="13">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">930[0-9].*\(rfi\|remote\|include\)</field>
<description>OWASP#1 Remote File Inclusion bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_access_control,rfi,</group>
</rule>
<!-- Restricted File Access (952xx) -->
<rule id="100252" level="11">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">952[0-9]</field>
<description>OWASP#1 Unauthorized File Access bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_access_control,file_access,</group>
</rule>
<!-- ═══════════════════════════════════════════════════════════
OWASP #8 : SOFTWARE & DATA INTEGRITY (XXE, Deserialization)
═══════════════════════════════════════════════════════════ -->
<!-- XML External Entity (XXE) (943xx) -->
<rule id="100260" level="13">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">943[0-9]</field>
<description>OWASP#8 XML External Entity (XXE) bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_data_integrity,xxe,</group>
</rule>
<!-- ═══════════════════════════════════════════════════════════
OWASP #9 : LOGGING & MONITORING FAILURES (Scanner Detection)
═══════════════════════════════════════════════════════════ -->
<!-- Web Application Scanner Detection (951xx, 954xx) -->
<rule id="100270" level="10">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">951[0-9]\|954[0-9]</field>
<description>OWASP#9 Web Scanner / Bot detected — $(transaction.client_ip)</description>
<group>owasp_monitoring,scanner_detection,</group>
</rule>
<!-- ═══════════════════════════════════════════════════════════
AUTRES : SSRF, Open Redirect, Malicious File Upload
═══════════════════════════════════════════════════════════ -->
<!-- Server-Side Request Forgery (970xx, 971xx) -->
<rule id="100280" level="13">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">970[0-9]\|971[0-9]</field>
<description>OWASP Server-Side Request Forgery (SSRF) bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_misc,ssrf,</group>
</rule>
<!-- Insecure File Upload (953xx) -->
<rule id="100281" level="12">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">953[0-9]</field>
<description>OWASP Insecure File Upload bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_misc,file_upload,</group>
</rule>
<!-- ═══════════════════════════════════════════════════════════
FRÉQUENCE & PATTERNS RÉPÉTÉS
═══════════════════════════════════════════════════════════ -->
<!-- SQLi répétées (brute-force injection) -->
<rule id="100290" level="14" frequency="3" timeframe="60">
<if_matched_sid>100220</if_matched_sid>
<description>OWASP#3 SQL Injection brute-force — $(transaction.client_ip) tentatives répétées</description>
<group>owasp_injection,sql_injection,brute_force,</group>
</rule>
<!-- XSS répétées (scanner ou attaque soutenue) -->
<rule id="100291" level="13" frequency="5" timeframe="120">
<if_matched_sid>100240</if_matched_sid>
<description>OWASP#7 XSS attack pattern — $(transaction.client_ip) attaque soutenue</description>
<group>owasp_auth,xss,</group>
</rule>
<!-- RCE attempts répétées (critiques) -->
<rule id="100292" level="15" frequency="2" timeframe="60">
<if_matched_sid>100222</if_matched_sid>
<description>OWASP#3 Command Injection attempts — $(transaction.client_ip) CRITICAL</description>
<group>owasp_injection,command_injection,critical,</group>
</rule>
<!-- Path Traversal brute-force -->
<rule id="100293" level="12" frequency="4" timeframe="180">
<if_matched_sid>100250</if_matched_sid>
<description>OWASP#1 Path Traversal brute-force — $(transaction.client_ip)</description>
<group>owasp_access_control,lfi,brute_force,</group>
</rule>
<!-- ═══════════════════════════════════════════════════════════
RÈGLES REMONTÉES (100100 - tous les events, bloqués ou non)
Patterns OWASP détectés sans être bloqués
═══════════════════════════════════════════════════════════ -->
<!-- OWASP#3 SQLi remontée (942xx - non bloquée, anomaly scoring) -->
<rule id="100320" level="9">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">942[0-9]</field>
<description>OWASP#3 SQL Injection attempt (anomaly scoring) — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_injection,sql_injection,warning,</group>
</rule>
<!-- OWASP#3 Command Injection remontée (932xx - non bloquée) -->
<rule id="100321" level="10">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">932[0-9]</field>
<description>OWASP#3 Command Injection attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_injection,command_injection,warning,</group>
</rule>
<!-- OWASP#3 LDAP Injection remontée (942xx LDAP) -->
<rule id="100322" level="9">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">942[0-9].*LDAP\|ldap_filter</field>
<description>OWASP#3 LDAP Injection attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_injection,ldap_injection,warning,</group>
</rule>
<!-- OWASP#3 PHP/Java Injection remontée (933xx, 934xx) -->
<rule id="100323" level="9">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">933[0-9]\|934[0-9]</field>
<description>OWASP#3 Code Injection attempt (PHP/Java) — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_injection,code_injection,warning,</group>
</rule>
<!-- OWASP#6 RCE/Exploit remontée (930xx, 950xx - non bloquée) -->
<rule id="100330" level="11">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">930[0-9]\|950[0-9]</field>
<description>OWASP#6 RCE / Exploit attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_vulnerable_components,rce,warning,</group>
</rule>
<!-- OWASP#7 XSS remontée (941xx - non bloquée) -->
<rule id="100340" level="8">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">941[0-9]</field>
<description>OWASP#7 XSS attempt (anomaly scoring) — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_auth,xss,warning,</group>
</rule>
<!-- OWASP#7 Header Injection remontée (955xx) -->
<rule id="100341" level="8">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">955[0-9]</field>
<description>OWASP#7 HTTP Header Injection attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_auth,header_injection,warning,</group>
</rule>
<!-- OWASP#1 Path Traversal remontée (930xx LFI patterns) -->
<rule id="100350" level="8">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">930[0-9].*\(path\|traversal\|lfi\)</field>
<description>OWASP#1 Path Traversal attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_access_control,lfi,warning,</group>
</rule>
<!-- OWASP#1 RFI remontée (930xx RFI patterns) -->
<rule id="100351" level="9">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">930[0-9].*\(rfi\|remote\|include\)</field>
<description>OWASP#1 Remote File Inclusion attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_access_control,rfi,warning,</group>
</rule>
<!-- OWASP#1 File Access remontée (952xx) -->
<rule id="100352" level="8">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">952[0-9]</field>
<description>OWASP#1 Unauthorized File Access attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_access_control,file_access,warning,</group>
</rule>
<!-- OWASP#8 XXE remontée (943xx) -->
<rule id="100360" level="9">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">943[0-9]</field>
<description>OWASP#8 XML External Entity (XXE) attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_data_integrity,xxe,warning,</group>
</rule>
<!-- OWASP#9 Scanner remontée (951xx, 954xx) -->
<rule id="100370" level="6">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">951[0-9]\|954[0-9]</field>
<description>OWASP#9 Web Scanner / Bot detected — $(transaction.client_ip)</description>
<group>owasp_monitoring,scanner_detection,warning,</group>
</rule>
<!-- OWASP SSRF remontée (970xx, 971xx) -->
<rule id="100380" level="9">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">970[0-9]\|971[0-9]</field>
<description>OWASP Server-Side Request Forgery (SSRF) attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_misc,ssrf,warning,</group>
</rule>
<!-- OWASP File Upload remontée (953xx) -->
<rule id="100381" level="8">
<if_sid>100100</if_sid>
<field name="transaction.messages.message">953[0-9]</field>
<description>OWASP Insecure File Upload attempt — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>owasp_misc,file_upload,warning,</group>
</rule>
<!-- ═══════════════════════════════════════════════════════════
FRÉQUENCE — Attaques par anomaly scoring répétées
═══════════════════════════════════════════════════════════ -->
<!-- SQLi anomaly scoring répétée -->
<rule id="100394" level="10" frequency="5" timeframe="300">
<if_matched_sid>100320</if_matched_sid>
<description>OWASP#3 SQL Injection anomaly scoring — $(transaction.client_ip) tentatives répétées</description>
<group>owasp_injection,sql_injection,anomaly,</group>
</rule>
<!-- XSS anomaly scoring répétée -->
<rule id="100395" level="10" frequency="8" timeframe="300">
<if_matched_sid>100340</if_matched_sid>
<description>OWASP#7 XSS anomaly scoring — $(transaction.client_ip) attaque soutenue</description>
<group>owasp_auth,xss,anomaly,</group>
</rule>
<!-- Command Injection tentatives répétées -->
<rule id="100396" level="12" frequency="2" timeframe="60">
<if_matched_sid>100321</if_matched_sid>
<description>OWASP#3 Command Injection attempts — $(transaction.client_ip) CRITICAL</description>
<group>owasp_injection,command_injection,anomaly,</group>
</rule>
</group>
@@ -0,0 +1,44 @@
<group name="web,modsecurity,attack,">
<!-- Règles spécialisées par type d'attaque — basées sur local_modsecurity_rules.xml (rule 100102) -->
<!-- SQL Injection bloquée -->
<rule id="100202" level="12">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">SQL Injection\|SQLi\|942[0-9]</field>
<description>ModSecurity: SQL Injection bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>modsecurity_sqli,sql_injection,</group>
</rule>
<!-- XSS bloqué -->
<rule id="100203" level="12">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">XSS\|941[0-9]</field>
<description>ModSecurity: XSS bloqué — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>modsecurity_xss,xss,</group>
</rule>
<!-- Remote Code Execution bloqué -->
<rule id="100204" level="14">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">RCE\|930[0-9]\|932[0-9]</field>
<description>ModSecurity: tentative RCE bloquée — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>modsecurity_rce,rce,</group>
</rule>
<!-- Local File Inclusion bloqué -->
<rule id="100205" level="12">
<if_sid>100102</if_sid>
<field name="transaction.messages.message">LFI\|930[0-9]</field>
<description>ModSecurity: LFI bloqué — $(transaction.client_ip) → $(transaction.request.uri)</description>
<group>modsecurity_lfi,lfi,</group>
</rule>
<!-- Blocages répétés — attaque soutenue -->
<rule id="100210" level="14" frequency="5" timeframe="120">
<if_matched_sid>100102</if_matched_sid>
<description>ModSecurity: blocages répétés en 2 min — attaque soutenue</description>
<group>modsecurity_repeated,</group>
</rule>
</group>
@@ -0,0 +1,129 @@
<!-- Règles WireGuard VPN — IDs 100800-100826
Les logs kernel passent par rule 5100 (program_name=^kernel).
On hérite via if_sid 5100 et on filtre par match/regex.
Les rules 1082x sont des enfants pour les peers de confiance
(etc/lists/wireguard-trusted-ips) : le negate="yes" sur <list>
n'est pas supporté par analysisd ici, donc on matche positivement
et on laisse la règle la plus spécifique (l'enfant) l'emporter
sur le parent.
Niveau 3 (pas 0) : <log_alert_level>3</log_alert_level> dans
ossec.conf exclut tout niveau < 3 de l'index OpenSearch. Un niveau
0 rendrait les handshakes de confiance invisibles du dashboard VPN
(compteurs handshakes/peers actifs à 0). Le groupe vpn_trusted
permet de les filtrer d'une vue "alertes à traiter" sans les
retirer des stats. -->
<group name="wireguard,vpn,">
<!-- Keepalive : level 0 = pas d'index OpenSearch -->
<rule id="100800" level="0">
<if_sid>5100</if_sid>
<match>wireguard: wg0: </match>
<regex>keepalive packet</regex>
<description>WireGuard: keepalive peer $(srcip)</description>
<group>wireguard,vpn_keepalive,</group>
</rule>
<!-- Handshake initiation -->
<rule id="100801" level="3">
<if_sid>5100</if_sid>
<match>wireguard: wg0: Sending handshake initiation</match>
<description>WireGuard: handshake initiation vers peer $(srcip)</description>
<group>wireguard,vpn_handshake,</group>
</rule>
<rule id="100821" level="3">
<if_sid>100801</if_sid>
<list field="srcip" lookup="match_key">etc/lists/wireguard-trusted-ips</list>
<description>WireGuard: handshake initiation - peer de confiance $(srcip)</description>
<group>wireguard,vpn_handshake,vpn_trusted,</group>
</rule>
<!-- Handshake réponse envoyée -->
<rule id="100802" level="3">
<if_sid>5100</if_sid>
<match>wireguard: wg0: Sending handshake response</match>
<description>WireGuard: handshake reponse envoyee a peer $(srcip)</description>
<group>wireguard,vpn_handshake,</group>
</rule>
<rule id="100822" level="3">
<if_sid>100802</if_sid>
<list field="srcip" lookup="match_key">etc/lists/wireguard-trusted-ips</list>
<description>WireGuard: handshake reponse - peer de confiance $(srcip)</description>
<group>wireguard,vpn_handshake,vpn_trusted,</group>
</rule>
<!-- Handshake réponse reçue (session établie) -->
<rule id="100803" level="3">
<if_sid>5100</if_sid>
<match>wireguard: wg0: Receiving handshake response</match>
<description>WireGuard: handshake complet avec peer $(srcip)</description>
<group>wireguard,vpn_handshake,</group>
</rule>
<rule id="100823" level="3">
<if_sid>100803</if_sid>
<list field="srcip" lookup="match_key">etc/lists/wireguard-trusted-ips</list>
<description>WireGuard: handshake complet - peer de confiance $(srcip)</description>
<group>wireguard,vpn_handshake,vpn_trusted,</group>
</rule>
<!-- Handshake timeout (pas d'exclusion : un timeout reste un signal utile,
même pour un peer de confiance) -->
<rule id="100810" level="7">
<if_sid>5100</if_sid>
<match>wireguard: wg0: Handshake for peer</match>
<regex>did not complete</regex>
<description>WireGuard: handshake timeout peer $(srcip)</description>
<group>wireguard,vpn_warning,</group>
</rule>
<!-- Timeout répété >= 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</description>
<group>wireguard,vpn_disconnected,</group>
</rule>
<!-- Timeout grave >= 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 60s</description>
<group>wireguard,vpn_disconnected,</group>
</rule>
<!-- Peer roaming -->
<rule id="100815" level="5">
<if_sid>5100</if_sid>
<match>wireguard: wg0: Peer </match>
<regex>is now roaming</regex>
<description>WireGuard: peer $(id) roaming vers $(srcip)</description>
<group>wireguard,vpn_roaming,</group>
</rule>
<rule id="100825" level="3">
<if_sid>100815</if_sid>
<list field="srcip" lookup="match_key">etc/lists/wireguard-trusted-ips</list>
<description>WireGuard: roaming - peer de confiance $(id) vers $(srcip)</description>
<group>wireguard,vpn_roaming,vpn_trusted,</group>
</rule>
<!-- Reconnexion après déconnexion -->
<rule id="100816" level="6">
<if_sid>5100</if_sid>
<match>wireguard: wg0: Receiving handshake response</match>
<description>WireGuard: peer $(srcip) reconnecte</description>
<group>wireguard,vpn_reconnected,</group>
</rule>
<rule id="100826" level="3">
<if_sid>100816</if_sid>
<list field="srcip" lookup="match_key">etc/lists/wireguard-trusted-ips</list>
<description>WireGuard: reconnexion - peer de confiance $(srcip)</description>
<group>wireguard,vpn_reconnected,vpn_trusted,</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,369 @@
#!/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 ────────────────────────────────────────────────────────────────────
# Note: pour un peer de la liste de confiance (etc/lists/wireguard-trusted-ips),
# analysisd indexe l'alerte sous le rule.id de la regle enfant 1082x (voir
# wireguard-rules.xml), pas celui du parent 1008xx. Il faut donc inclure les
# deux jeux d'IDs partout, sinon les peers de confiance disparaissent des stats.
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 OR rule.id: 100816 "
"OR rule.id: 100821 OR rule.id: 100822 OR rule.id: 100823 OR rule.id: 100825 OR rule.id: 100826")
Q_HANDSHAKES = ("rule.id: 100801 OR rule.id: 100802 OR rule.id: 100803 "
"OR rule.id: 100821 OR rule.id: 100822 OR rule.id: 100823")
Q_WARNINGS = "rule.id: 100810 OR rule.id: 100811 OR rule.id: 100812"
# Peers actifs : cardinalite sur data.srcip, tous rule.id de handshake confondus
# (100802/100822 = reponse envoyee, le cas dominant sur un serveur qui recoit les
# connexions ; 100803/100823 = reponse recue, cote initiateur).
Q_COMPLETE = Q_HANDSHAKES
# ── 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": True,
"timeFrom": "now-24h",
"timeTo": "now",
"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 Firewall" 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_FW = "rule.id: 651"
Q_ALL = Q_WEB + " OR " + Q_FW
# ── 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] Firewall - Total Bans",
"type": "metric",
"query": Q_FW,
"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 firewall", "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] Firewall - IPs Bannies",
"type": "table",
"query": Q_FW,
"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 Firewall ===\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 Firewall",
"description": "Alertes OWASP ModSecurity brute-force API et bans Firewall",
"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()
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Crée des Saved Queries réutilisables pour la détection d'attaques web
(ModSecurity/OWASP, rules 100100-100396 — voir
config/wazuh_manager/rules/modsecurity_owasp_rules.xml) dans OpenSearch
Dashboards (Wazuh).
Usage:
docker cp create-saved-queries-web.py wazuh_dashboard:/tmp/
docker exec wazuh_dashboard python3 /tmp/create-saved-queries-web.py
Une fois créées, ouvrir n'importe quelle vue (Security Events, Discover...) >
barre de recherche > icone dossier (Saved Queries) > sélectionner la query.
"""
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!")
# ── Queries ───────────────────────────────────────────────────────────────────
QUERIES = [
{
"id": "web-attacks-all",
"title": "Attaques Web - Toutes",
"description": "Tous les événements ModSecurity/OWASP (bloqués + remontés en anomaly scoring).",
"kuery": "rule.groups: web",
},
{
"id": "web-attacks-blocked",
"title": "Attaques Web - Bloquées (WAF)",
"description": "Requêtes réellement bloquées par le WAF (exclut les anomaly scoring non bloquants, groupe 'warning').",
"kuery": "rule.groups: web AND NOT rule.groups: warning",
},
{
"id": "web-attacks-critical",
"title": "Attaques Web - Critiques",
"description": "Niveau >= 13 : SQLi/RCE/command injection bloquées, XXE, SSRF, patterns répétés (owasp_injection critical/brute_force).",
"kuery": "rule.groups: web AND rule.level >= 13",
},
{
"id": "web-attacks-scanners",
"title": "Attaques Web - Scanners / Recon",
"description": "Détection de scanners/bots (OWASP#9 — groupe scanner_detection).",
"kuery": "rule.groups: scanner_detection",
},
{
"id": "web-attacks-repeated",
"title": "Attaques Web - Patterns répétés (brute-force)",
"description": "Attaques soutenues détectées par fréquence : SQLi (100290), XSS (100291), RCE (100292), path traversal (100293), anomaly scoring répété (100394-100396).",
"kuery": "rule.id: 100290 OR rule.id: 100291 OR rule.id: 100292 OR rule.id: 100293 OR rule.id: 100394 OR rule.id: 100395 OR rule.id: 100396",
},
]
# ── 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 main():
print("=== Creation Saved Queries: Attaques Web ===\n")
failed = False
for q in QUERIES:
body = {
"attributes": {
"title": q["title"],
"description": q["description"],
"query": {"language": "kuery", "query": q["kuery"]},
"filters": [],
}
}
r = api("POST", f"/api/saved_objects/query/{q['id']}?overwrite=true", body)
if "id" in r:
print(f" OK {q['title']}")
else:
print(f" ERR {q['title']}: {r}")
failed = True
if failed:
sys.exit(1)
print("\n=== Termine ===")
if __name__ == "__main__":
main()
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
Crée une Saved Query "Sans VPN" réutilisable dans OpenSearch Dashboards (Wazuh).
Objectif : permettre d'exclure en un clic les alertes WireGuard/VPN (handshakes,
timeouts, roaming, reconnexions — rules 100800-100826) de la vue native
"Security Events" / "Threat Hunting", qui liste par défaut TOUTES les alertes
sans filtre de groupe. Le dashboard dédié "VPN - Activite WireGuard"
(create-dashboard-vpn.py) continue lui d'afficher ces événements normalement :
cette query ne supprime rien de l'index, elle filtre uniquement l'affichage.
Usage:
docker cp create-saved-query-no-vpn.py wazuh_dashboard:/tmp/
docker exec wazuh_dashboard python3 /tmp/create-saved-query-no-vpn.py
Une fois créée, ouvrir "Security Events" > barre de recherche > icône
"Saved Queries" (dossier) > sélectionner "Sans VPN".
"""
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!")
QUERY_ID = "no-vpn-alerts"
QUERY_TITLE = "Sans VPN"
QUERY_DESC = "Exclut les alertes WireGuard/VPN (handshakes, timeouts, roaming) de la vue Security Events."
QUERY_KUERY = "NOT rule.groups: wireguard"
# ── 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 main():
print("=== Creation Saved Query: Sans VPN ===\n")
body = {
"attributes": {
"title": QUERY_TITLE,
"description": QUERY_DESC,
"query": {"language": "kuery", "query": QUERY_KUERY},
"filters": [],
}
}
r = api("POST", f"/api/saved_objects/query/{QUERY_ID}?overwrite=true", body)
if "id" in r:
print(f" OK Saved Query '{QUERY_TITLE}' créée ({r['id']})")
print(" Utilisation : Security Events > barre de recherche > icone dossier (Saved Queries) > 'Sans VPN'")
else:
print(f" ERR {r}")
sys.exit(1)
if __name__ == "__main__":
main()
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CERTS_DIR="$SCRIPT_DIR/certs"
mkdir -p "$CERTS_DIR"
cd /tmp
echo "[*] Téléchargement de l'outil de génération de certificats Wazuh..."
curl -sO https://packages.wazuh.com/4.14/wazuh-certs-tool.sh
cp "$SCRIPT_DIR/config/certs.yml" config.yml
echo "[*] Génération des certificats..."
bash wazuh-certs-tool.sh -A
echo "[*] Copie des certificats..."
cp /tmp/wazuh-certificates/*.pem "$CERTS_DIR/"
cp /tmp/wazuh-certificates/root-ca.pem "$CERTS_DIR/root-ca-manager.pem"
chmod 640 "$CERTS_DIR"/*.pem
rm -rf /tmp/wazuh-certificates /tmp/wazuh-install-files /tmp/wazuh-install-files.tar /tmp/wazuh-certs-tool.sh /tmp/config.yml
echo "[OK] Certificats générés dans $CERTS_DIR"
+2
View File
@@ -0,0 +1,2 @@
Soc-1:AqGx3H0O9RJqfRR5Wzw7W!
Soc-2:P493A81j3bsSXCupftpVJI!