Wazuh, écris la règle qui capte ce que tu cherches vraiment

Écrire une règle Wazuh custom, ce n'est pas tout couvrir. C'est poser une question précise à un log réel, la tester avec wazuh-logtest, puis la déployer.

Wazuh, écris la règle qui capte ce que tu cherches vraiment

À l’épisode précédent, tu as appris à lire ton SIEM. Tu as ouvert l’audit de configuration, restreint la surveillance de fichiers à ce qui compte, trié tes vulnérabilités par sévérité. Trois usages, trois questions, des réponses nettes. Sauf que toutes ces alertes, tu ne les as pas écrites. Elles sortaient du jeu de règles fourni par Wazuh, des milliers de règles pensées par d’autres, pour des cas génériques.

Aujourd’hui, tu passes de l’autre côté. Tu écris ta première règle.

Et je vais te dire tout de suite ce qui distingue une bonne règle custom d’une mauvaise, parce que c’est tout le sujet. Une mauvaise règle part d’un fantasme, tout couvrir, ne rien rater, verrouiller l’univers. Une bonne règle part d’un log réel que tu as sous les yeux et d’une question précise que tu lui poses. Une seule question. C’est exactement le fil rouge de la série, du plus calme au plus bavard, sauf qu’ici tu tiens le stylo.

Ce tuto vise la branche 4.x de Wazuh, la même que toute la série, version de référence 4.14.7. Prévois une petite heure, un log sous les yeux, et l’envie de résister à la tentation d’en faire trop.


Ce qu’il te faut

Rien de neuf à installer. Tout est hérité de la série.

  • Ton manager Wazuh 4.x opérationnel, celui du tout-en-un.
  • Au moins un agent en état Active qui remonte des logs.
  • Un accès shell au manager, parce que les règles vivent dans des fichiers, pas dans un formulaire.
  • Un vrai log qui t’intéresse. Pas un exemple de doc, un log de chez toi, celui pour lequel tu te dis « ça, je voudrais le savoir quand ça arrive ».

Ce dernier point n’est pas décoratif. Écrire une règle sans log réel devant soi, c’est deviner. Et une règle qui devine, elle se trompe.


Anatomie d’une règle, décodage, champ, niveau

Avant d’écrire, il faut comprendre le chemin que parcourt un log. Trois étapes, toujours les mêmes.

Un log arrive. Wazuh le pré-décode, il en extrait le squelette commun à tout message système, l’horodatage, le nom de la machine, le nom du programme. Puis un décodeur entre en scène et découpe le reste en champs nommés, un utilisateur, une adresse source, une action. Enfin, le log passe devant le mur des règles. La première règle dont toutes les conditions sont vraies gagne, et elle génère une alerte au niveau qu’elle porte.

Retiens cette distinction, c’est le cœur de tout ce qui suit. Une règle peut chercher de deux façons.

  • Avec <match> ou <regex>, elle fouille le texte brut du log, la ligne entière, telle quelle.
  • Avec <field>, elle interroge un champ déjà extrait par un décodeur, proprement isolé.

La première méthode, c’est le filet à grosses mailles. La seconde, c’est la pince à épiler. Une règle précise, celle qui pose une vraie question, s’accroche presque toujours à un décodeur et compare un champ nommé. On y revient.

Côté écriture, une règle est un bloc XML. Ses attributs principaux, tu n’en utiliseras que deux au début.

  • id, l’identifiant numérique unique de la règle.
  • level, sa sévérité, sur une échelle dont on parle plus bas.

Et ses balises enfant utiles pour commencer.

  • <if_sid>, le chaînage, « ne me déclenche que si telle autre règle a déjà matché ». La clé de la précision.
  • <field name="...">, la comparaison sur un champ décodé.
  • <match>, la recherche dans le texte brut.
  • <description>, la phrase qui s’affichera dans l’alerte. Écris-la pour toi dans six mois, pas pour la machine.

D’autres balises existent, frequency et timeframe pour compter les répétitions dans une fenêtre de temps, pcre2 pour les motifs costauds, decoded_as, tu les découvriras quand une question précise les réclamera. Pas avant. Garde la référence des options de règle ouverte le jour où tu écris, c’est la page qui liste tout, balise par balise.


Où vivent tes règles, et l’erreur qui te les efface

Deux fichiers, deux règles d’or.

Tes règles custom vont dans /var/ossec/etc/rules/local_rules.xml. Tes décodeurs custom, quand tu en auras besoin, dans /var/ossec/etc/decoders/local_decoder.xml.

Ce que tu ne touches jamais, c’est le répertoire /var/ossec/ruleset/. C’est le jeu de règles fourni par Wazuh, et il est réécrit intégralement à chaque mise à jour. Une personnalisation posée là-dedans disparaît à la première montée de version, sans un mot d’adieu. Tout ce qui t’appartient reste dans etc/.

Deuxième règle, les identifiants. Réserve à tes règles la plage 100000 à 120000. Le moteur accepte techniquement n’importe quel numéro jusqu’à 999999, mais rester au-dessus de 100000 t’évite d’entrer en collision avec les règles système. La borne haute, 120000, est une recommandation confortable, pas un mur.

Et si tu veux modifier une règle fournie plutôt qu’en créer une ? Tu la recopies dans ton fichier local avec l’attribut overwrite="yes", en gardant son identifiant. Attention, certaines balises structurelles comme if_sid ou if_group ne se laissent pas réécrire ainsi, dans ce cas tu crées une nouvelle règle qui s’appuie sur l’existante.


La méthode, du log réel à la règle testée

Voici la boucle. Tu vas la répéter à chaque règle que tu implémenteras, alors autant la créer proprement dès la première.

  1. Tu attrapes le log brut. La ligne exacte, copiée telle qu’elle apparaît, sans la reformuler.
  2. Tu regardes comment Wazuh la comprend déjà. Quels champs un décodeur en tire, quelle règle système la ramasse peut-être au passage.
  3. Tu écris ta règle dans local_rules.xml, en t’accrochant à ce qui existe.
  4. Tu la testes à froid, avant même de recharger le service, avec wazuh-logtest.
  5. Tu recharges le manager seulement quand le test est vert.

L’outil qui rend tout ça possible, c’est wazuh-logtest, à /var/ossec/bin/wazuh-logtest. Un mot pour les vétérans, si tu as croisé de vieux tutos parlant d’ossec-logtest, c’est le même outil, rebaptisé lors du passage d’OSSEC à Wazuh en version 4.0. Le nom d’aujourd’hui, c’est wazuh-logtest, l’ancien ne répondra plus.

Tu le lances, il t’invite à coller une ligne de log, et il rejoue sous tes yeux les trois phases dont on parlait, pré-décodage, décodage, matching. Le décodage est ton point de contrôle, c’est là que tu vérifies qu’un champ que tu veux interroger existe bien. Le matching t’annonce la règle qui l’emporte, son identifiant, son niveau, sa description.

Cette console, c’est ton banc d’essai. Tu peux y coller un log, voir qu’aucune règle ne le ramasse, écrire ta règle, recoller le log, et regarder la phase 3 changer. Sans jamais toucher à la production.

Sortie de wazuh-logtest sur une ligne de log Vaultwarden d'échec d'authentification : phase de décodage avec les champs srcip et srcuser extraits, phase de matching sur la règle 100011 au niveau 10.


Un exemple de bout en bout

Assez de théorie. Prenons une vraie question, la plus banale qui soit, et déroulons-la jusqu’au bout.

La question. « Je veux savoir quand quelqu’un rate l’authentification sur mon coffre Vaultwarden, et de quelle adresse il s’y essaie. » Une question, un service, un événement. Quelqu’un qui tape à côté du mot de passe de ton gestionnaire de mots de passe, c’est exactement le genre de bruit que tu veux entendre.

Le log sous les yeux. À chaque échec, Vaultwarden écrit cette ligne.

[2026-08-13 09:42:11.523][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: 203.0.113.42. Username: [email protected].

Regarde le format, ce n’est pas du syslog classique, pas de machine programme[pid]. Vaultwarden préfixe chaque ligne de [horodatage][module][niveau]. Retiens-le, ça va compter pour le décodeur.

Le premier réflexe, la grosse maille. Tu pourrais écrire une règle qui fouille le texte brut.

<group name="vaultwarden,">
  <rule id="100010" level="5">
    <match>Username or password is incorrect. Try again</match>
    <description>Vaultwarden, échec d'authentification</description>
  </rule>
</group>

Ça marche, ça sonne à chaque échec. Mais tu n’as qu’un fait nu, « quelqu’un s’est planté ». L’adresse et le compte visé sont là, dans la ligne, sauf que Wazuh ne les voit pas comme des champs. Impossible de compter les tentatives par adresse, de géolocaliser, de repérer l’acharné qui revient toutes les dix secondes. Tu as ouvert un robinet, pas posé une question.

La bonne maille, la pince à épiler. On repart de zéro. Pour interroger l’adresse et le compte, il te faut ces champs décodés. Or Vaultwarden ne fait pas partie des formats que Wazuh connaît d’origine, tu vas devoir le lui apprendre, la section suivante détaille le décodeur. Admettons qu’il extraie déjà srcip et srcuser. Ta règle devient alors chirurgicale.

<group name="vaultwarden,">
  <rule id="100010" level="0">
    <decoded_as>vaultwarden</decoded_as>
    <description>Vaultwarden, ligne reconnue, socle de détection</description>
  </rule>

  <rule id="100011" level="10">
    <if_sid>100010</if_sid>
    <match>Username or password is incorrect</match>
    <description>Vaultwarden, échec d'authentification de $(srcuser) depuis $(srcip)</description>
  </rule>
</group>

Regarde ce qui se joue là. La règle 100010 est en level="0", elle ne sonne pas, elle sert juste de socle, elle dit « cette ligne vient de Vaultwarden ». La règle 100011 s’y accroche via <if_sid>, ne se déclenche que sur l’échec d’authentification, et rappelle dans sa description le compte visé et l’adresse d’où vient la tentative.

Une question posée, une réponse donnée, et un silence total sur tout le reste. Et si demain tu ne veux d’alerte que pour un compte précis, tu remplaces ce <match> par un <field name="srcuser">[email protected]</field>, la pince se resserre encore. Le jour où tu voudras hurler quand la même adresse se plante dix fois en une minute, tu ajouteras frequency et timeframe par-dessus, mais ça, c’est une autre règle pour un autre jour.

Le test, avant de déployer quoi que ce soit.

/var/ossec/bin/wazuh-logtest

Colle ta ligne de log. En phase 3, tu dois voir sonner la règle 100011, au niveau 10, avec ta description remplie : Vaultwarden, échec d’authentification de [email protected] depuis 203.0.113.42. L’adresse est là, récupérée, prête à géolocaliser ou à corréler. Si tu veux valider en une commande, sans lire la sortie, l’option -U compare le résultat à un triplet attendu, identifiant, niveau, décodeur, et te rend un code de sortie propre.

/var/ossec/bin/wazuh-logtest -U 100011:10:vaultwarden

Puis tu recharges.

sudo systemctl restart wazuh-manager

Un redémarrage coupe brièvement la réception des logs, le temps que le service revienne. Sur un manager de PME, c’est indolore.

L’exemple 100010/100011 illustre la syntaxe exacte du chaînage if_sid, avec les champs décodés réinjectés dans la description. Avant de compter sur l’alerte en prod, colle-le dans wazuh-logtest et vérifie que les substitutions $(srcip) et $(srcuser) s’affichent bien. Trente secondes, et pas de coquille découverte le jour où l’alerte compte vraiment.


Quand il faut un décodeur, et quand il n’en faut surtout pas

C’est le piège du débutant zélé, écrire un décodeur pour tout. Ne le fais pas.

Le principe est simple. Si un décodeur existant sort déjà le champ qui t’intéresse, tu n’écris aucun décodeur. Tu écris directement une règle qui s’accroche à la règle existante et compare le champ. La plupart des formats standards, ceux de SSH, des services système, des serveurs connus, sont déjà décodés par Wazuh. Vérifie-le d’abord dans la phase 2 de wazuh-logtest, si ton champ apparaît, le travail est fait.

Tu n’écris un décodeur que dans un cas précis, quand Wazuh ne comprend pas le format d’un log maison ou exotique. La phase 2 du test reste vide, ou ne sort pas le champ que tu cherches ? Alors, et seulement alors, tu apprends à Wazuh à lire ce format.

Un décodeur custom, ça vit dans /var/ossec/etc/decoders/local_decoder.xml, et ça se compose de deux blocs, un parent qui reconnaît l’application, un enfant qui extrait les champs. Pour notre Vaultwarden, ça donne :

<decoder name="vaultwarden">
  <prematch>][vaultwarden::</prematch>
</decoder>

<decoder name="vaultwarden">
  <parent>vaultwarden</parent>
  <regex>IP: (\d+.\d+.\d+.\d+). Username: (\S+).</regex>
  <order>srcip, srcuser</order>
</decoder>

Le parent reconnaît la ligne à une sous-chaîne stable, ][vaultwarden::, le nom du module que Vaultwarden colle dans chacune de ses lignes. Pourquoi pas un <program_name> comme d’habitude ? Parce que Vaultwarden n’écrit pas en syslog, il n’y a pas de champ programme à accrocher. Si tu fais passer tes logs par journald ou syslog en amont, alors le préfixe standard réapparaît et tu peux revenir au <program_name>vaultwarden</program_name> classique.

En lecture directe du fichier ou de la sortie Docker, le <prematch> sur le module est ta prise la plus solide. L’enfant, lui, capture l’adresse puis le compte, dans cet ordre, et les nomme srcip et srcuser, exactement les champs que ta règle 100011 attend. Le nom srcip n’est pas un caprice, c’est celui que Wazuh sait géolocaliser et corréler. La boucle est bouclée.

Un avertissement sur les expressions régulières, parce que c’est là qu’on perd des heures. La syntaxe des motifs Wazuh n’est pas tout à fait la regex classique à laquelle tu penses, le moteur par défaut (OS_Regex) a ses propres conventions. Teste chaque décodeur dans wazuh-logtest avant de t’y fier, et considère l’exemple ci-dessus comme un patron à valider sur ton format réel, pas comme du copier-coller garanti.


Le vrai danger, ce n’est pas la syntaxe, c’est le nombre

Tu sais maintenant écrire une règle. La partie technique est derrière toi. Reste la partie qui tue les SIEM, la retenue.

Chaque règle que tu ajoutes est une bouche de plus qui parlera. Le réflexe, une fois qu’on sait écrire, c’est d’écrire trop, une règle pour ci, une pour ça, au cas où. Trois semaines plus tard, ton flux ressemble au mur indistinct dont l’épisode 1 t’avait sorti, sauf que cette fois, c’est toi qui l’as bâti.

D’où la seule règle d’hygiène qui compte, la même qu’au premier épisode.

Ne crée jamais une règle dont tu ne sais pas quoi faire quand elle sonnera. Si ta seule réaction possible à l’alerte est de hausser les épaules, cette règle n’a pas de raison d’exister.

Le level est ton outil de dosage, il va de 0 à 15, le 16 existe mais reste non documenté. Sers-t’en sérieusement.

  • Le 0 est silencieux. Il décode et observe sans jamais réveiller personne. C’est le niveau des règles socles, comme la 100010 de l’exemple. Parfaitement légitime, et sous-utilisé.
  • Les niveaux bas, de 2 à 7, sont le bruit de fond, erreurs mineures, tentatives sans gravité.
  • Le milieu, de 8 à 11, mérite un œil.
  • Le haut, de 12 à 14, ce sont les événements de sécurité importants.
  • Le 15 est l’attaque sévère, celle qui appelle une réaction immédiate, sans faux positif attendu.

N’invente pas de signification pour le 16, non documenté. Et surtout, ne colle pas un niveau élevé à une règle par excès de zèle. Un level haut n’a de sens que si une action concrète est prévue quand il tombe. Sinon, tu fabriques de l’angoisse, pas de la sécurité.


Si ça ne marche pas

Problème, ta règle ne se déclenche jamais dans le test

Cause probable, le champ que tu interroges n’est pas décodé, ou la règle parente référencée dans <if_sid> ne matche pas ce log.

Solution, relance wazuh-logtest avec l’option verbeuse -v et lis la phase 2. Si ton champ n’y apparaît pas, aucun décodeur ne le produit, il te faut donc en écrire un avant d’écrire ta règle. Vérifie aussi que l’identifiant dans <if_sid> correspond bien à une règle qui, elle, ramasse le log.

Problème, wazuh-logtest voit ta règle mais le manager l’ignore en production

Cause probable, le service n’a pas été rechargé depuis que tu as modifié le fichier.

Solution, sudo systemctl restart wazuh-manager. L’outil de test lit tes fichiers directement, le moteur de production, lui, travaille sur le jeu de règles chargé en mémoire. Tant que tu ne recharges pas, il ne connaît pas ta nouvelle règle.

Problème, ta règle a disparu après une mise à jour de Wazuh

Cause probable, tu l’avais écrite dans /var/ossec/ruleset/ au lieu de /var/ossec/etc/rules/.

Solution, réécris-la dans local_rules.xml et ne remets plus jamais les pieds dans ruleset/. Ce répertoire est terrain de Wazuh, réécrit à chaque montée de version.


Pour les pressés

Ce que fait cet article. Il t’apprend à écrire ta première règle de détection Wazuh, en partant d’un log réel et d’une question précise, à la tester hors production, puis à la déployer sans casser quoi que ce soit.

Concrètement, la marche à suivre.

  1. Attrape le log brut qui t’intéresse et lance /var/ossec/bin/wazuh-logtest pour voir quels champs Wazuh en extrait déjà.
  2. Écris ta règle dans /var/ossec/etc/rules/local_rules.xml, identifiant entre 100000 et 120000, jamais dans ruleset/.
  3. Privilégie la précision, accroche-toi à une règle existante avec <if_sid> et compare un champ décodé avec <field>, plutôt que de fouiller le texte brut avec <match>.
  4. Si le champ voulu n’est pas décodé, et seulement dans ce cas, écris un décodeur dans /var/ossec/etc/decoders/local_decoder.xml.
  5. Teste dans wazuh-logtest jusqu’au vert, au besoin avec -U identifiant:niveau:decodeur pour une validation en une commande.
  6. Recharge, sudo systemctl restart wazuh-manager.
  7. Règle d’or, n’écris aucune règle dont tu ne sais pas quoi faire quand elle sonnera. Le level 0 pour observer sans alerter est ton ami.

En résumé

Tu es passé de lecteur à auteur. Tu ne subis plus les règles écrites par d’autres, tu poses tes propres questions à tes propres logs.

Et la leçon n’est pas dans la syntaxe XML, que tu maîtriseras en trois règles. Elle est dans la retenue. Une bonne règle vise un log précis, chaîne sur ce qui existe déjà, et se tait sur le reste. Le décodeur, tu ne l’écris que forcé. Le niveau d’alerte, tu le dis honnêtement. Et chaque règle que tu ajoutes, tu la mérites par une action que tu es prêt à mener quand elle sonnera.

Choisis peu de questions. Réponds-y vraiment. C’est valable pour lire, c’est valable pour écrire.


Pour creuser, la doc officielle

Toute la syntaxe que tu viens de manipuler est documentée, page par page. Garde ces liens sous la main, ce sont tes sources.


Et après, montre à ton SIEM ce que tu bloques

Tu sais lire ton SIEM, tu sais maintenant lui apprendre de nouvelles questions. Le prochain cran, c’est de lui donner à voir un signal qu’il ne capte pas encore, ce que tes Mac laissent tourner, et surtout ce qu’ils refusent.

Au prochain épisode, on branche le contrôle d’exécution de tes Mac sur Wazuh, pour que chaque blocage devienne une alerte lisible dans le même tableau de bord que le reste. Le geste d’écriture que tu viens d’apprendre y sera ton outil de base.

À lire bientôt : Brancher Santa sur Wazuh, corréler ce qui s’exécute et ce qu’on bloque

Et pour l’épisode qui précède, celui où l’on apprend à lire avant d’écrire.

À lire : Ton SIEM souverain, lis-le vraiment puis fais-le passer à l’action

Des termes techniques ? Consulte le glossaire.