Chapitre 718 minIntermédiaire
Écrire une règle Wazuh personnalisée et un décodeur
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- tester une ligne de journal avec wazuh-logtest ;
- écrire une règle personnalisée accrochée à une règle existante ;
- écrire un décodeur pour le journal d'une application ;
- réduire un faux positif sans aveugler la détection ;
- changer le niveau d'une règle fournie avec overwrite.
3 exercices corrigés
Une règle Wazuh personnalisée est une règle que tu écris dans /var/ossec/etc/rules/local_rules.xml pour adapter la détection à ton organisation : relever le niveau de ce qui compte chez toi, repérer un enchaînement propre à ton infrastructure, écarter un faux positif précis. Quand le journal vient d'une application que Wazuh ne connaît pas, tu écris d'abord un décodeur dans local_decoder.xml. Ce chapitre apprend à tester avec wazuh-logtest, puis à écrire une règle, un décodeur et une exception, en évitant les erreurs classiques. Il suppose que tu sais lire une règle et une alerte (chapitre 6).
Tester avec wazuh-logtest
Avant de toucher à une règle, on teste. L'outil wazuh-logtest rejoue une ligne de journal dans le moteur d'analyse et montre les trois phases :
sudo /var/ossec/bin/wazuh-logtest
Colle la ligne, valide : l'outil affiche la phase 1 (horodatage, machine, programme), la phase 2 (nom du décodeur et champs extraits), la phase 3 (identifiant, niveau, description et groupes de la règle retenue), puis indique si une alerte serait générée. Pour tester une règle de fréquence, colle plusieurs lignes à la suite dans la même session : l'outil garde la mémoire des événements précédents. Le tableau de bord offre la même chose dans Server management > Ruleset Test.
wazuh-logtest lit les fichiers de règles à chaque session : une règle que tu viens d'enregistrer se teste tout de suite, sans redémarrer. Le serveur, lui, ne l'appliquera aux vrais événements qu'après un redémarrage de wazuh-manager.
Écrire une règle personnalisée
Tes règles vont dans /var/ossec/etc/rules/local_rules.xml (ou dans un autre fichier du même dossier), jamais dans /var/ossec/ruleset/rules/, que chaque mise à jour remplace. Trois règles du jeu :
- des identifiants entre 100000 et 120000, réservés aux règles locales ;
- chaque règle dans un bloc
<group name="…,">(la virgule finale est d'usage) ; - une règle personnalisée s'accroche de préférence à une règle existante (
if_sid), qui a déjà fait le travail de décodage et de tri.
Exemple 1 : un échec SSH sur root qui ne vient pas du VLAN d'administration. Chez Sorbier, seul le VLAN 99 a le droit d'administrer srv-web en SSH. Une tentative sur root venue d'ailleurs est donc anormale, même isolée : elle mérite plus que le niveau 5 de la règle 5760.
<group name="sorbier,sshd,">
<rule id="100100" level="10">
<if_sid>5760</if_sid>
<user>^root$</user>
<srcip negate="yes">192.168.10.208/28</srcip>
<description>Sorbier : échec SSH sur root hors du VLAN d'administration</description>
<mitre>
<id>T1110.001</id>
</mitre>
<group>authentication_failed,</group>
</rule>
</group>
La règle ne s'évalue que si 5760 a correspondu, et seulement si l'utilisateur décodé est exactement root et que l'adresse source n'est pas dans 192.168.10.208/28. Pour la ligne suivie au chapitre 6 (Failed password for root from 192.168.10.71), elle devient la règle la plus précise : l'alerte est de niveau 10. Garder le groupe authentication_failed permet aux règles de corrélation qui comptent ce groupe de continuer à la compter.
Exemple 2 : un groupe d'administration modifié hors des heures ouvrées. L'ajout d'un compte au groupe local Administrateurs (règle 60154) ou au groupe Admins du domaine (règle 60159) est déjà de niveau 12. La nuit, chez Sorbier, personne n'administre l'annuaire : c'est presque sûrement une attaque.
<group name="sorbier,windows,">
<rule id="100110" level="14">
<if_sid>60154,60159</if_sid>
<time>7 pm - 7 am</time>
<description>Sorbier : groupe d'administration modifié en dehors des heures ouvrées</description>
<mitre>
<id>T1098</id>
</mitre>
<group>group_changed,</group>
</rule>
</group>
<time> compare l'heure du serveur Wazuh, pas l'horodatage de l'événement : une raison de plus pour que toutes les machines soient à l'heure et dans le même fuseau. Une seconde règle avec <weekday>weekends</weekday> couvrirait les samedis et dimanches.
Enregistre, teste avec wazuh-logtest quand c'est possible, puis applique :
sudo systemctl restart wazuh-manager
sudo tail -n 20 /var/ossec/logs/ossec.log # aucune erreur de chargement des règles ?
Si le XML est invalide (balise non fermée), le serveur refuse de démarrer et ossec.log donne le fichier et la ligne : corrige avant tout. Un identifiant en double ne bloque pas le démarrage : Wazuh écrit un avertissement (« Rule ID … is duplicated ») et ignore la seconde règle, sans autre signe. Le tableau de bord permet aussi d'éditer local_rules.xml depuis Server management > Rules, dans la liste des fichiers personnalisés.
Écrire un décodeur
Une règle ne peut travailler que sur des champs décodés. Pour un journal que Wazuh ne connaît pas, il faut d'abord un décodeur. Le site de suivi des colis, sur srv-web, écrit ses événements de connexion à l'espace client dans journald :
Nov 3 09:14:22 srv-web suivi-colis[812]: LOGIN_FAILED user=client4471 ip=198.51.100.23
Dans /var/ossec/etc/decoders/local_decoder.xml :
<decoder name="suivi-colis">
<program_name>^suivi-colis$</program_name>
</decoder>
<decoder name="suivi-colis-champs">
<parent>suivi-colis</parent>
<regex>^(\w+) user=(\S+) ip=(\S+)</regex>
<order>action, dstuser, srcip</order>
</decoder>
Le décodeur parent s'applique aux lignes dont le programme s'appelle exactement suivi-colis. L'enfant découpe le message : (\w+) capture un mot (LOGIN_FAILED), puis (\S+) une suite de caractères sans espace, deux fois. <order> nomme les captures dans l'ordre, avec les noms de champs standard de Wazuh (action, dstuser, srcip), que les règles savent tester directement. La syntaxe par défaut des expressions est celle de Wazuh (OS_Regex), plus limitée que PCRE : \w, \d, \s, \S, +, *, ^, $ et les parenthèses suffisent pour l'essentiel ; ajoute type="pcre2" pour une expression plus riche.
Puis, dans local_rules.xml, les règles qui s'en servent :
<group name="sorbier,suivi-colis,">
<rule id="100120" level="0">
<decoded_as>suivi-colis</decoded_as>
<description>Suivi des colis : événements de l'application</description>
</rule>
<rule id="100121" level="5">
<if_sid>100120</if_sid>
<action>LOGIN_FAILED</action>
<description>Suivi des colis : échec de connexion à l'espace client</description>
<group>authentication_failed,</group>
</rule>
<rule id="100122" level="10" frequency="10" timeframe="120">
<if_matched_sid>100121</if_matched_sid>
<same_srcip />
<description>Suivi des colis : échecs de connexion répétés depuis la même adresse</description>
<mitre>
<id>T1110</id>
</mitre>
<group>authentication_failures,</group>
</rule>
</group>
La règle 100120, de niveau 0, regroupe tout ce que produit l'application sans alerter : c'est le parent auquel s'accrochent les autres, comme la règle 5700 pour SSH. Si les développeurs peuvent écrire leurs journaux en JSON, tout cela devient inutile : le décodeur JSON intégré extrait chaque clé, et les règles testent directement <field name="evenement">.
Réduire un faux positif
Le copieur multifonction de la logistique (192.168.10.90) dépose les numérisations sur srv-fichiers avec le compte de service svc-scan, dont le mot de passe a changé sans que le copieur soit mis à jour. Résultat : un 4625 toutes les cinq minutes, une alerte 60122 à chaque fois, et des alertes de force brute qui finissent par ne plus être lues.
La bonne réponse est d'abord de corriger la cause : mettre à jour le mot de passe dans le copieur. En attendant l'intervention, on écarte ce faux positif précis, et lui seul, avec une règle de niveau 0 accrochée à la règle bruyante :
<group name="sorbier,faux-positifs,">
<rule id="100130" level="0">
<if_sid>60122</if_sid>
<field name="win.eventdata.targetUserName" type="pcre2">(?i)^svc-scan$</field>
<field name="win.eventdata.ipAddress" type="pcre2">^192\.168\.10\.90$</field>
<description>Faux positif connu : copieur de la logistique, compte svc-scan (ticket 2026-1187)</description>
</rule>
</group>
Les deux champs doivent correspondre : un échec de svc-scan depuis une autre adresse, ou un échec d'un autre compte depuis le copieur, continue de lever l'alerte. La description cite le ticket pour que l'exception soit retirée une fois la cause corrigée.
Changer le niveau d'une règle fournie. Pour qu'un journal Windows effacé (règle 60117, niveau 9) parte par courriel au-delà du seuil de 12, recopie la règle entière dans local_rules.xml, change son niveau et ajoute overwrite="yes" :
<group name="sorbier,windows,">
<rule id="60117" level="12" overwrite="yes">
<if_sid>60100</if_sid>
<field name="win.system.eventID">^1102$</field>
<description>Windows audit log was cleared</description>
<options>no_full_log</options>
<group>logs_cleared,pci_dss_10.6.1,gpg13_10.1,gdpr_IV_35.7.d,hipaa_164.312.b,nist_800_53_AU.6,tsc_CC7.2,tsc_CC7.3,</group>
<mitre>
<id>T1685.005</id>
</mitre>
</rule>
</group>
Tout est recopié, y compris la ligne <group> complète : sans les groupes de conformité (pci_dss, gdpr…), les alertes 60117 disparaîtraient des tableaux de bord de conformité. On en profite pour corriger la technique ATT&CK : la règle fournie porte T1070.004, une erreur d'étiquette expliquée au chapitre 10. La règle d'origine reste intacte dans ruleset/, et ta version la remplace même après une mise à jour.
Les erreurs fréquentes
| Erreur | Conséquence | La bonne pratique |
|---|---|---|
Modifier un fichier de /var/ossec/ruleset/ | La modification disparaît à la mise à jour suivante | local_rules.xml, et overwrite="yes" pour changer une règle fournie |
Réutiliser un identifiant existant sans overwrite | Ta règle est ignorée (avertissement dans ossec.log), sans autre signe | Identifiants de 100000 à 120000, tenus dans un tableau |
| Écrire une règle sans parent, sur tout le texte du message | Lente, et elle correspond à des journaux imprévus | S'accrocher à une règle existante avec if_sid ou decoded_as |
Oublier de redémarrer wazuh-manager | La règle marche dans wazuh-logtest, pas sur les vrais événements | Redémarrer, puis vérifier ossec.log |
| Mettre au niveau 0 un événement bruyant entier | La détection est aveuglée | Une exception étroite, ou mieux : corriger la cause |
Exercices
Exercice 1 sur 3 Écrire une règle pour une ligne donnée Réussi À revoir
Le compte deploy sert à déployer le site sur srv-web, uniquement pendant les heures ouvrées. Sur ce serveur, une connexion réussie produit cette ligne, que Wazuh classe avec la règle 5715 « sshd: authentication success. », de niveau 3, en décodant l'utilisateur et l'adresse source :
Nov 4 23:41:07 srv-web sshd-session[5120]: Accepted password for deploy from 192.168.10.71 port 50322 ssh2
Écris une règle de niveau 12 qui se déclenche quand deploy se connecte avec succès entre 19 h et 8 h, et une seconde de niveau 12 quand il se connecte depuis une adresse hors du VLAN d'administration, à toute heure.
Voir la réponseMasquer la réponse de l'exercice 1
<group name="sorbier,sshd,">
<rule id="100140" level="12">
<if_sid>5715</if_sid>
<user>^deploy$</user>
<time>7 pm - 8 am</time>
<description>Sorbier : connexion SSH du compte deploy en dehors des heures ouvrées</description>
<mitre>
<id>T1078</id>
</mitre>
<group>authentication_success,</group>
</rule>
<rule id="100141" level="12">
<if_sid>5715</if_sid>
<user>^deploy$</user>
<srcip negate="yes">192.168.10.208/28</srcip>
<description>Sorbier : connexion SSH du compte deploy hors du VLAN d'administration</description>
<mitre>
<id>T1078</id>
</mitre>
<group>authentication_success,</group>
</rule>
</group>
Les deux s'accrochent à 5715, qui a déjà reconnu une connexion réussie et décodé l'utilisateur. La ligne de l'énoncé (23 h 41, depuis 192.168.10.71) remplit les deux conditions : seule la première règle qui correspond dans l'arbre donne l'alerte, mais les deux sont de niveau 12, et chacune couvre un cas que l'autre ne voit pas. T1078 Valid Accounts : un compte légitime utilisé de façon anormale, peut-être par quelqu'un qui a volé son mot de passe. Une meilleure correction de fond : n'autoriser deploy qu'avec une clé SSH et depuis le VLAN 99 (AllowUsers deploy@192.168.10.208/28 dans sshd_config).
Tu avais trouvé ?
Exercice 2 sur 3 Un décodeur et sa règle Réussi À revoir
Le script de sauvegarde du site, sur srv-web, écrit cette ligne quand une sauvegarde échoue :
Nov 4 02:15:07 srv-web sauvegarde-site[4410]: ECHEC destination=srv-fichiers motif=espace_insuffisant
Écris le décodeur (parent et enfant) qui extrait le résultat dans le champ action et le motif dans extra_data, puis une règle de niveau 8 qui alerte sur chaque échec.
Voir la réponseMasquer la réponse de l'exercice 2
Dans local_decoder.xml :
<decoder name="sauvegarde-site">
<program_name>^sauvegarde-site$</program_name>
</decoder>
<decoder name="sauvegarde-site-champs">
<parent>sauvegarde-site</parent>
<regex>^(\w+) destination=\S+ motif=(\S+)</regex>
<order>action, extra_data</order>
</decoder>
Dans local_rules.xml :
<group name="sorbier,sauvegarde,">
<rule id="100150" level="0">
<decoded_as>sauvegarde-site</decoded_as>
<description>Sauvegarde du site : événements du script</description>
</rule>
<rule id="100151" level="8">
<if_sid>100150</if_sid>
<action>ECHEC</action>
<description>Sauvegarde du site : échec de la sauvegarde</description>
</rule>
</group>
destination=\S+ est reconnu sans être capturé, puisqu'on ne demande que deux champs. On teste avec wazuh-logtest : la phase 2 doit afficher action: 'ECHEC' et extra_data: 'espace_insuffisant', la phase 3 la règle 100151. Un échec de sauvegarde n'est pas une attaque, mais c'est une menace sur la disponibilité : un rançongiciel qui frapperait le lendemain trouverait une sauvegarde absente (cours Sauvegarde et PRA). Cette alerte intéresse autant la supervision que le SIEM.
Tu avais trouvé ?
Exercice 3 sur 3 Une exception trop large Réussi À revoir
Pour faire taire les alertes du copieur, un collègue a écrit l'exception ci-dessous. Explique pourquoi elle est dangereuse et corrige-la.
<rule id="100131" level="0">
<if_sid>60122</if_sid>
<field name="win.eventdata.ipAddress">192.168.10</field>
<description>Bruit réseau interne</description>
</rule>
Voir la réponseMasquer la réponse de l'exercice 3
L'expression 192.168.10 (sans ancre) correspond à toute adresse du réseau interne : tous les échecs de connexion venant de l'intérieur deviennent invisibles, et les règles de force brute qui comptent le groupe authentication_failed ne les comptent plus. Or c'est précisément depuis un poste interne compromis qu'un attaquant essaie des mots de passe. La description vague ne permet pas non plus de savoir pourquoi l'exception existe ni quand la retirer. La version correcte est la règle 100130 du chapitre : le compte et l'adresse exacts du copieur, ancrés (^…$), avec une description qui cite le ticket.
Tu avais trouvé ?