Retour d'expérienceLecture : 4 min

Le bilan du matin qui oubliait 184 alertes critiques

J'avais simulé 184 alertes critiques, le tableau de bord de Wazuh les montrait, et le bilan du matin n'en comptait aucune : voici comment une source incomplète et un bloc except silencieux se sont cumulés.

Publié
Juin 2026
Contexte
Production · PME, SIEM Wazuh, script de bilan quotidien
Projet lié
SIEM durci et alertes enrichies par IA locale

Le contexte

Le SIEM de l'entreprise, une PME de distribution d'une vingtaine de personnes, envoyait chaque matin à 9 h un bilan des 24 heures écoulées. Un script le produisait : volumes par gravité, graphiques, machines les plus actives, types d'alertes fréquents et une fiche pour chaque type d'alerte critique.

Un bilan quotidien sert à savoir, sans ouvrir le tableau de bord, ce que la journée a produit. S'il se trompe, il se trompe avec l'apparence d'un document fiable.

Le symptôme

La veille, j'avais volontairement simulé 184 alertes de niveau 12 ou plus. Elles étaient bien visibles dans le tableau de bord de Wazuh. Le bilan du matin annonçait 358 alertes de niveau 8 ou plus, et aucune de niveau 11 ou 12. Plusieurs modules, pourtant validés, n'y apparaissaient jamais.

Le script s'était exécuté et le bilan était arrivé. Rien ne signalait un problème.

Ce que le symptôme permet de comprendre

  • Le bilan comptait 358 alertes : le script tournait et lisait des données. Ce n'était pas une panne franche, mais un comptage partiel.
  • Les alertes simulées étaient dans le tableau de bord et absentes du bilan : les deux ne lisaient pas la même chose, ou pas en entier.
  • Le chiffre manquant était connu d'avance : 184. Un test au résultat attendu transforme un doute en écart mesurable. La question devient : que lit le script, et que fait-il de ce qu'il ne comprend pas ?

Le diagnostic, pas à pas

En relisant le code, j'ai trouvé deux défauts qui se cumulaient. Le script lisait un fichier de journal local, ligne par ligne, et décodait chaque ligne en JSON. Autour du décodage, un bloc except avalait en silence toute erreur de lecture : une ligne illisible disparaissait, et rien ne le signalait.

Schéma simplifié du défaut (pseudo-code, pas le script réel)

for ligne in fichier_local:        # source locale, incomplète
    try:
        alerte = json.loads(ligne)
    except Exception:
        continue                   # ligne perdue, aucune trace
    compter(alerte)

Restait à savoir ce que contenait vraiment la source. Une requête directe à l'indexer de Wazuh, la base OpenSearch qui alimente le tableau de bord, donnait bien le chiffre du tableau de bord. Le fichier local, lui, n'en contenait qu'une partie.

Avant et après la réécriture du bilan quotidien : la première version lisait un fichier local incomplet et ignorait les erreurs JSON ; la seconde interroge l'indexer Wazuh, la même source que le tableau de bord. Première version Fichier local ne contient qu'une partie des alertes Script du bilan lecture ligne par ligne Bloc except erreurs JSON ignorées en silence Bilan du matin 358 alertes ≥ 8 0 de niveau 11–12 Après réécriture Indexer Wazuh source de vérité (OpenSearch) Script réécrit une seule fonction de requête Agrégations côté serveur (volume Suricata) Bilan du matin volumes conformes au tableau de bord
Fig. 1Le calcul était juste, la source était incomplète : le bilan réécrit interroge l'indexer, la même source que le tableau de bord.

La cause, simplement

Le calcul était juste. La source était incomplète, et le script ne disait jamais ce qu'il perdait. Ensemble, les deux défauts produisaient un bilan plausible, et faux.

Un except silencieux est dangereux pour une raison simple : il transforme une erreur en absence de données. Un script qui s'arrête sur une erreur se remarque. Un script qui ignore ses erreurs produit un résultat crédible, et un bilan de sécurité qui sous-compte les alertes critiques rassure à tort. En sécurité, un faux « rien à signaler » coûte plus cher qu'une panne visible.

La correction : changer de source

Plutôt que de mieux lire le fichier, j'ai réécrit le script pour interroger directement l'indexer, la même source que le tableau de bord. La présentation du bilan n'a pas changé. Toutes les sections passent par une seule fonction d'authentification et de requête : une seule porte d'entrée vers les données, la même pour tout le bilan.

Au passage, j'ai corrigé trois autres points :

  • L'ordre chronologique des fiches d'alertes critiques, rétabli.
  • L'adresse source des alertes Syslog : le champ habituel n'existe pas pour elles, un repli va la chercher ailleurs.
  • Le volume Suricata, qui plafonnait à la taille maximale d'une requête : il est désormais calculé par l'indexer lui-même, par agrégation côté serveur.

J'ai aussi ajouté une corrélation : une adresse IP vue par plusieurs sources différentes en 24 heures est un signal plus fort qu'une détection isolée. Le bilan réécrit donne des volumes conformes au tableau de bord.

Ce que j'en retiens

  • Tester un outil de sécurité avec un résultat attendu. Ce sont les 184 alertes simulées qui ont révélé l'écart. Injecter un volume connu, puis vérifier qu'on le retrouve, est un test simple pour toute chaîne de reporting.
  • Lire la même source que l'outil de référence. Un bilan doit interroger la base qui alimente le tableau de bord, pas une copie locale dont on ne maîtrise pas le contenu.
  • Ne jamais avaler une erreur en silence. Un bloc except doit au moins compter ce qu'il ignore et le dire dans le résultat : « N lignes illisibles » vaut mieux qu'un chiffre faux présenté comme exact.