UTMStack v11.2.8 Community Edition — Procédures de déploiement pour lab PME Suisse. Installation VMware, Suricata, CrowdSec, SOAR, OPNsense.
Ce chapitre documente le projet qui a occupé la majeure partie de plusieurs sessions de travail : brancher un LLM local (Ollama) sur les alertes UTMStack pour produire un résumé quotidien exploitable, sans dépendance cloud. Le résultat final fonctionne bien — mais le chemin pour y arriver est le vrai contenu de cette page. Douze versions de pipeline, plusieurs échecs de classification révélateurs, une leçon d’architecture qui dépasse largement ce lab, et une bascule de modèle en production décidée après coup sur la base de tests concrets.
ℹ️ À qui s’adresse ce chapitre. Si tu cherches juste à déployer le pipeline chez toi, va directement à l’annexe technique de déploiement. Cette page-ci raconte pourquoi le pipeline est construit comme il l’est — utile si tu veux comprendre les pièges avant de les reproduire, ou si tu veux adapter l’approche à ton propre contexte.
Le SOC AI natif d’UTMStack Community Edition produit un résumé pauvre et générique — un texte du type “AI GENERATED ANALYSIS: Multiple related alerts were detected…“, quel que soit le modèle branché derrière. La cause a été confirmée par reverse engineering du backend (SocAIService.analyzeAlert(), extraction du .war) : un vrai appel API externe existe bien, mais le prompt système est codé en dur côté UTMStack, invisible et non modifiable depuis l’extérieur.
En creusant dans le schéma PostgreSQL, une colonne est apparue sans être exploitée nulle part : rule_description dans utm_correlation_rules. Pour chaque règle de corrélation, elle contient une description complète avec une section Next Steps détaillée (10+ étapes d’investigation) et des références MITRE ATT&CK — jamais affichée dans l’UI, jamais utilisée par le SOC AI natif.
Ce champ inexploité est devenu le point de départ du projet : construire un pipeline externe (n8n) qui interroge les vraies données d’alertes UTMStack, exploite rule_description, et produit un résumé réellement contextualisé — avec un objectif de départ non négociable : tout doit rester local, aucune donnée ne sort du lab.
Ce chapitre documente d’abord cette architecture 100% locale (sections 2 à 9), qui reste la référence et le pipeline de production principal. Les tests comparatifs menés en section 9 ont cependant fait émerger un résultat qu’il aurait été malhonnête de passer sous silence : un fournisseur cloud basé en UE peut égaler, voire dépasser, la qualité d’analyse du meilleur modèle local testé, pour un coût mensuel dérisoire. La section 10 documente cette variante cloud optionnelle — jamais comme un remplacement du pipeline local, mais comme un second choix légitime, avec ses propres compromis explicites. La section 13, ajoutée après quelques jours de tests répétés en conditions réelles, documente un second changement : la bascule du moteur local lui-même, de Qwen vers Llama, pour une raison précise et testée.
Le pipeline existe en deux variantes, qui partagent la même logique :
| Variante | Déclenchement | Modèle | Usage | Notification |
|---|---|---|---|---|
| Rapport quotidien | Schedule Trigger, 6h00 (Europe/Zurich) | Llama 3.1 8B (historiquement Qwen 2.5 14B — voir section 13) | Consultation du matin, pas de contrainte de temps | Email + stockage PostgreSQL |
| À la demande | Webhook n8n | Llama 3.1 8B (historiquement Qwen 2.5 14B) | Vérification ponctuelle, réponse synchrone via webhook | Réponse HTTP immédiate, pas d’email |
ℹ️ Pourquoi le rapport quotidien envoie un email et pas celui à la demande. Le rapport quotidien tourne sans supervision directe (déclenché par un Schedule Trigger à 6h00) — sans notification, sa seule trace est une ligne de plus dans la table
resumes_soc, qu’il faut penser à aller consulter. Le rapport à la demande, lui, est déclenché manuellement via webhook et répond immédiatement dans le navigateur ou le client HTTP qui l’a appelé — une notification email serait redondante avec la réponse déjà reçue en synchrone.
Stack technique complète : Ollama natif sur l’hôte Windows (accès direct au CPU/futur GPU sans passthrough), n8n pour l’orchestration, PostgreSQL pour le stockage des rapports et la lecture des rule_description, OpenSearch (v11-alert-*) comme source de données, plus SearXNG et Open WebUI pour l’usage conversationnel complémentaire (voir section 8).
┌──────────────────┐
│ Schedule (6h) │
│ ou Webhook │
└─────────┬──────────┘
│
▼
┌─────────────────────────────┐
│ OpenSearch v11-alert-* │ agrégations 24h : sévérité,
│ (HTTP Request) │ catégorie MITRE, signatures,
└─────────┬───────────────────── IP source/cible + géoloc
│
▼
┌─────────────────────────────┐
│ PostgreSQL │ utm_correlation_rules
│ rule_description │ (procédures officielles,
└─────────┬───────────────────── Next Steps, MITRE)
│
▼
┌───────────────────────────────────────────┐
│ TRI DÉTERMINISTE (JavaScript) │
│ ───────────────────────────── │
│ • Classement SIGNAL / CONTRÔLE / │
│ BRUIT / INDÉTERMINÉ (mots-clés) │
│ • Déduplication par signature │
│ • Flag "rareté suspecte" (≤2 occurrences) │
│ • Construction du SQUELETTE pré-rédigé │
│ avec emplacements [COMMENTAIRE_N] │
└─────────┬────────────────────────────────────
│
▼
┌─────────────────────────────┐
│ IPs Signaux (split) │ une IP → un item
└─────────┬─────────────────────
│
┌─────┼─────┬─────┬───────────────┐
▼ ▼ ▼ ▼ ▼
┌─────┐┌─────┐┌─────┐┌──────┐ ┌───────────────┐
│Abuse││Grey ││ OTX ││Threat│ │ Historique │
│IPDB ││Noise││ ││ Fox │ │ SOC (30j) │
└──┬──┘└──┬──┘└──┬──┘└──┬───┘ └──────┬────────┘
│ │ │ │ │
└──────┴──────┴──────┴─────────────┘
│
▼
┌─────────────────────────┐
│ Merge TI (Append, │ ⚠️ le mode "Combine"
│ 5 entrées) │ ne gère que 2 entrées !
└────────────┬──────────────┘
│
▼
┌─────────────────────────┐
│ Aggregation TI │ regroupe par IP :
│ (JavaScript) │ score, classification,
└────────────┬──────────────── pulses, récurrence
│
▼
┌─────────────────────────┐
│ Fusion dans le │ injection des données
│ squelette (JS) │ TI sous chaque IP
└────────────┬──────────────┘
│
▼
┌─────────────────────────┐
│ Ollama — Llama 3.1 8B │ COMPLÉTION PURE :
│ (HTTP Request) │ remplit [COMMENTAIRE_N],
└────────────┬──────────────── ne reclasse rien,
│ n'invente aucun nom
▼
┌─────────────────────────┐
│ Contrôle de complétion │ balise restante ?
│ (JavaScript) │ → ⚠️ avertissement visible
└────────────┬──────────────┘ (jamais de nettoyage
│ silencieux)
▼
┌────────────┴────────────┐
▼ ▼
┌──────────────┐ ┌─────────────────────┐
│ PostgreSQL │ │ Email (SMTP) │
│ resumes_soc │ │ quotidien uniquement │
└──────┬───────┘ └─────────────────────┘
▼
┌─────────────────────────┐
│ Webhook / Dashboard │ consultation web
└─────────────────────────┘
Le principe qui traverse tout le schéma : tout ce qui est encadré en amont d’Ollama est déterministe et auditable — du code, pas du LLM. Le modèle n’intervient qu’à un seul endroit précis, pour une seule tâche précise (compléter un texte déjà structuré), jamais pour classer ni pour décider. Ce principe n’est pas arrivé du premier coup — la section suivante raconte comment on y est arrivé.
ℹ️ Note de lecture. Cette section documente le parcours réel avec Qwen 2.5 14B, le modèle utilisé pendant toute la phase de construction et de stabilisation du pipeline. La bascule de production vers Llama 3.1 8B (section 13) est intervenue après coup, pour une raison précise et différente des échecs racontés ici — elle ne remet pas en cause ce qui suit, qui reste la trace fidèle de comment cette architecture a été construite.
Les premières versions du pipeline interrogeaient l’index v11-log-* — les logs Suricata bruts. Erreur de conception discrète mais lourde de conséquences : ce n’est pas l’index que consulte le dashboard UTMStack. Le bon index pour les alertes de corrélation est v11-alert-*, avec des champs bien spécifiques (name.keyword, severityLabel.keyword, category.keyword, technique.keyword, adversary.ip.keyword, target.ip.keyword).
Cette correction a immédiatement révélé un problème de fond resté invisible jusque-là : la règle High level Suricata alert générait à elle seule 300 à 1000+ alertes par jour, classées “High”, catégorie “Initial Access” — et il s’est avéré que ~95% de ce volume était en réalité du trafic ET INFO Windows Update P2P Activity et GNU/Linux APT User-Agent Outbound. Un faux positif massif, invisible tant que le pipeline travaillait sur le mauvais index.
⚠️ Piège à retenir.
adversary.ipettarget.ipsuivent la direction du flux réseau, pas le rôle attaquant/victime. Sur certaines signatures (notamment celles détectant une réponse sortante), l’IP interne apparaît commeadversaryet l’IP externe commetarget— l’inverse de l’intuition. À vérifier au cas par cas plutôt que de supposer.
Face à un premier test où Mistral NeMo 12B classait un webshell actif (ET WEB_SERVER WebShell Generic - wget http - POST) en “bruit de reconnaissance passive”, la réponse naturelle a été de durcir le prompt : liste explicite de termes obligatoirement prioritaires, interdictions formelles, règle de non-contradiction.
Résultat du test suivant : le modèle a bien identifié la signature ET COMPROMISED en section signal — puis a écrit dans la même réponse “il n’y a pas de signal nécessitant une vérification humaine prioritaire”, se contredisant dans son propre texte. Le prompt engineering seul avait amélioré le comportement sans le corriger : le modèle appliquait la contrainte localement, mais l’oubliait trois phrases plus tard.
Le changement de philosophie qui a débloqué le projet : sortir la classification du LLM et la coder en JavaScript.
const TERMES_SIGNAL = [
"WebShell", "EXPLOIT", "COMPROMISED", "MALWARE", "TROJAN",
"ATTACK_RESPONSE", "CNC", "Cobalt Strike", "Empire",
"Remote Command Execution", "Suspicious String", "sinkhole",
"Backdoor", "Ransomware", "Shellcode", "Phishing", "Exfiltration",
"Lateral Movement", "Mimikatz", "PsExec", "Reverse Shell"
];
Une simple correspondance de mots-clés, testée unitairement sur les signatures réelles du lab avant d’être appliquée, classe chaque signature en SIGNAL / CONTRÔLE / BRUIT / INDÉTERMINÉ. Le LLM reçoit alors des données déjà triées et se contente de rédiger — plus de décision de classification à sa charge.
Testé sur trois modèles (Llama 3.1 8B, Mistral NeMo 12B, Qwen 2.5 14B), le résultat a validé l’approche : les trois produisaient des rapports cohérents une fois la classification retirée de leur responsabilité. Un bug de déduplication est apparu au passage — la même signature citée par deux règles de corrélation différentes se retrouvait comptée (et affichée) deux fois — corrigé en fusionnant par nom de signature avec somme des occurrences.
ℹ️ Le principe qui en ressort. Détection déterministe, narration probabiliste. C’est le même partage des rôles qu’un SIEM industriel : les règles de corrélation détectent, le LLM (quand il y en a un) rédige. Confondre les deux mène à des rapports qui se contredisent.
Le tri déterministe seul ne suffisait pas encore. Sur un run réel, un modèle a produit une signature qui n’existait dans aucune donnée source :
CUSTOM DROP - r00ts3c COMPROMISED DVR attempt
Ce nom n’apparaît nulle part dans les logs. Le modèle avait fusionné deux noms réels distincts — CUSTOM DROP - r00ts3c ViewLog.asp DVR exploit attempt et ET COMPROMISED Known Compromised or Hostile Host Traffic group 19 — en un troisième nom halluciné, plausible en apparence, inventé en réalité.
La correction a poussé le principe du tri déterministe à son terme logique : le code JavaScript ne se contente plus de classer, il rédige le squelette complet du rapport, avec les noms de signatures, les chiffres, les règles parentes déjà écrits en dur. Le LLM ne reçoit plus que des emplacements [COMMENTAIRE_N] à remplir :
**CUSTOM DROP - r00ts3c ViewLog.asp DVR exploit attempt** — 1 occurrence(s)
- Règle(s) : Exploit Attempt Detection | sévérité High | T1210
- IP sources principales : 115.84.178.56 [Vietnam] (1)
- Cibles internes : 192.168.1.203 (1)
- Procédure officielle (extrait) : [...]
[COMMENTAIRE_4]
Sa tâche devient de la pure complétion — reproduire le texte à l’identique, remplacer chaque balise par 2-3 phrases d’analyse. Plus aucune possibilité structurelle d’inventer un nom.
⚠️ Instruction de remplacement, ajoutée plus tard. Un run en production a montré qu’un modèle peut respecter la consigne de complétion sans pour autant supprimer la balise : il écrit son commentaire juste à côté de
[COMMENTAIRE_N]au lieu de remplacer ce texte, ce qui déclenche à tort l’avertissement de génération incomplète alors que le contenu est bien présent. La correction est une instruction de prompt explicite, ajoutée après le remplacement du texte à recopier : “chaque balise doit être REMPLACÉE, jamais recopiée ni laissée visible à côté — la chaîne exacte ne doit apparaître nulle part dans la réponse finale.” Ce comportement a été observé aussi bien chez Qwen que chez Llama (voir section 13) ; l’instruction de remplacement corrige les deux.
| Modèle | Comportement observé sur la tâche de complétion |
|---|---|
| Llama 3.1 8B | Le plus fiable sur la fidélité des noms (aucune altération observée) et sur la tenue de la consigne jusqu’au bout d’une génération longue (voir section 13) — mais laisse parfois la balise [COMMENTAIRE_N] visible à côté de son propre commentaire au lieu de la supprimer (corrigé par l’instruction de remplacement ci-dessus) |
| Mistral NeMo 12B | Correct une fois le tri déterministe en place, mais tendance à minimiser les signaux confirmés dans les versions antérieures au tri |
| Qwen 2.5 14B | Le plus riche en contexte et le mieux rédigé sur les premiers éléments d’un rapport, mais le plus “créatif” — a inventé une section entière (“NEXT STEPS”) en pompant le contexte PostgreSQL brut fourni en trop grande quantité, et tend à recycler un commentaire générique mot pour mot sur les éléments en fin de rapport à fort volume (voir section 13) |
Un enseignement contre-intuitif s’est dégagé de ce comparatif : sur une tâche de pure complétion, le modèle le plus docile bat parfois le plus capable. Qwen, plus intelligent, prenait des initiatives non désirées ; Llama, plus simple, exécutait la consigne à la lettre. La correction a fini par retirer la source de la dérive plutôt que de changer de modèle : le contexte PostgreSQL brut (15 rule_description en vrac) a été remplacé par l’injection d’une seule description ciblée, celle de la règle concernée, directement dans le squelette au bon endroit. Une fois cette source de confusion supprimée, les deux modèles se sont stabilisés — jusqu’à ce qu’un test à plus fort volume révèle une limite plus profonde chez Qwen (section 13).
TERMES_SIGNAL, TERMES_CONTROLE et TERMES_BRUIT ne sont pas une liste théorique — ils viennent d’une lecture empirique des signatures Suricata réellement rencontrées dans ce lab, catégorisées à la main par familiarité avec la nomenclature du ruleset Emerging Threats (ET) :
| Préfixe / mot-clé Suricata | Ce qu’il signale en général | Catégorie retenue |
|---|---|---|
ET WEB_SERVER, ET EXPLOIT, ET COMPROMISED, ET ATTACK_RESPONSE |
Exploitation active, hôte compromis confirmé | SIGNAL |
ET DROP, ET CINS, Dshield, Spamhaus |
Blocage sur réputation IP déjà neutralisé | CONTROLE |
ET INFO, SURICATA STREAM/HTTP/TCPv4 |
Bruit de décodage protocolaire ou trafic informatif | BRUIT |
Tout le reste (ex : SCAN Slow port scan detected) |
Ambigu par nature | INDÉTERMINÉ (laissé au jugement du LLM, avec flag de rareté si peu fréquent) |
La méthode pour l’étendre à un autre lab est la même : observer les signatures qui remontent réellement, pas deviner à l’avance. Deux sources concrètes pour trouver la signification d’une signature inconnue :
ET * suivent une nomenclature stable par catégorie (ET WEB_SERVER, ET POLICY, ET SCAN, ET MALWARE…), consultable sur rules.emergingthreats.netmsg de la règle Suricata elle-même — visible directement dans OpenSearch (lastEvent.log.alert.signature), généralement assez explicite pour classer sans ambiguïté⚠️ Cette liste vit, elle n’est jamais figée. Chaque nouvelle source de données (agent Windows plus riche, intégration O365, tests offensifs Kali) fait apparaître des signatures que la liste actuelle ne couvre pas — elles tombent alors par défaut en INDÉTERMINÉ, visibles mais non priorisées automatiquement. C’est un filet de sécurité, pas un défaut : mieux vaut une signature ignorée du tri mais visible, qu’une signature mal classée silencieusement. La liste doit être revue à chaque élargissement significatif du périmètre de collecte.
Ce lab prévoit justement plusieurs élargissements à court terme — intégrations Office 365/Azure (voir chapitre Intégrations) et tests offensifs avec Kali Linux. Une version mise à jour de
TERMES_SIGNAL/BRUIT/CONTROLE, enrichie avec les signatures découvertes lors de ces deux chantiers, sera publiée dans le dépôt une fois ces tests réalisés. En attendant, la liste fournie ici reste la version validée sur le périmètre réseau/Suricata uniquement.
La maintenance de cette liste n’est pas théorique. Le jour même de la rédaction de ce chapitre, l’activation du SMTP sur UTMStack (pour l’envoi de ses propres notifications par e-mail) a produit 489 alertes en une heure, toutes sur la même signature : NF - Outbound mail setup command EHLO. La cause, une fois investiguée : UTMStack envoyait une notification par e-mail à chaque déclenchement de règle — y compris pour la règle de heartbeat décrite en section 10 — et chaque notification déclenchait à son tour cette signature Suricata, dans une boucle auto-entretenue sans risque réel mais bruyante.
Ce cas illustre concrètement le principe : la signature ne figurait dans aucune des trois listes (ni SIGNAL, ni BRUIT, ni CONTRÔLE), elle est tombée en INDÉTERMINÉ, visible dans les rapports plutôt que silencieusement ignorée — ce qui a permis de la repérer et de comprendre la boucle le jour même. La correction n’a pas eu lieu dans TERMES_SIGNAL mais en amont, directement dans Suricata (threshold.config, directive suppress ciblée sur l’IP source plutôt que sur l’IP de destination — les relais Microsoft 365 tournant sur plusieurs plages IP, une exclusion par destination unique aurait été contournée dès le lendemain par un relais différent).
Aucun LLM, local ou non, n’a de mécanisme intrinsèque pour dire “je ne sais pas” — un modèle génératif complète du texte selon des probabilités, il ne vérifie pas des faits. La différence entre un LLM local 8-14B et un modèle massif n’est pas qu’un local “n’y arrive pas du tout” — c’est que le seuil auquel il tient une contrainte logique sur toute une génération est beaucoup plus bas, et que le coût pour repousser ce seuil (modèle 70B+, fine-tuning sur corpus propre, GPU professionnel à 15-30k CHF, mois de travail) est hors de portée d’un lab domestique.
C’est cette limite précise, observée concrètement, qui a motivé le passage au tri déterministe. Avant que le tri JavaScript n’existe (versions v1 à v4, section 3), Llama 3.1 8B, Mistral NeMo 12B et Qwen 2.5 14B — livrés à eux-mêmes sur les mêmes données brutes qu’aujourd’hui — ont chacun échoué différemment sur la même tâche de classification : un webshell confirmé classé en bruit de reconnaissance passive, une conclusion qui se contredit dans la même réponse, un nom de signature entièrement halluciné par fusion de deux signatures réelles. Le tri déterministe n’a pas été introduit par principe abstrait — il a été introduit parce que trois modèles locaux différents ont chacun raté cette même tâche, de trois façons différentes.
La confirmation est venue bien plus tard, une fois le pipeline stabilisé : les tests de la section 9, menés sur la même tâche non déterministe, ont montré que même un modèle local nettement plus récent et plus gros — DeepSeek-R1-Distill-32B, sorti après cette génération de modèles et spécifiquement entraîné au raisonnement — échoue encore sur le point précis qui compte le plus : relier deux faits dispersés dans le texte pour construire une corrélation. Le problème n’était donc pas propre à Llama, Mistral ou Qwen 2.5 — c’est une limite qui persiste à travers plusieurs générations de modèles locaux de cette taille, tant que le calcul reste accessible sur du matériel domestique.
⚠️ GÉNÉRATION INCOMPLÈTE) plutôt que d’être nettoyé silencieusementℹ️ Le message central de ce chapitre. L’IA générative dans un SOC n’est pas ce qui détecte. Ce qui détecte, c’est le detection engineering (règles) et parfois le machine learning statistique classique (baseline comportementale). L’IA générative sert à rendre lisible et actionnable ce que la détection a déjà trouvé. Confondre les deux mène soit à des attentes déçues, soit à des investissements mal ciblés.
Une fois le tri et la rédaction stabilisés, la limite suivante est apparue : le rapport nommait une menace sans jamais dire si l’IP source avait une réputation connue. Quatre feeds gratuits ont été intégrés — AbuseIPDB, GreyNoise Community, AlienVault OTX, et ThreatFox (abuse.ch, sans clé requise) — appliqués uniquement aux IP des signaux prioritaires et des signatures non classées, pour préserver les quotas gratuits (1000 lookups/jour AbuseIPDB, ~50-100/jour GreyNoise).
Feeds volontairement écartés : Shodan/Censys (redondant avec la géolocalisation déjà native dans UTMStack), VirusTotal (quota trop restrictif pour un usage batch), MISP (pertinent mais trop lourd à déployer et alimenter pour ce lab).
⚠️ Pourquoi pas dans le RAG. Les feeds threat intelligence sont des listes d’IoC (IP, hash, domaines) — structurées, volatiles, sans aucune sémantique vectorielle. Les injecter dans une base vectorielle comme Qdrant gaspille du calcul d’embedding et noie le RAG de bruit au moment d’une vraie recherche sémantique. La bonne approche est un lookup structuré au moment de la génération du rapport, pas une indexation.
La partie la plus instructive de ce chantier n’était pas les feeds eux-mêmes, mais un piège d’architecture n8n qui a produit un comportement erratique difficile à diagnostiquer : sur certains runs, seuls 1 ou 2 feeds sur 4 apparaissaient dans le rapport final ; sur d’autres, les 3-4 étaient présents. Le premier réflexe a été de suspecter une condition de course (timing variable des appels API).
La vraie cause : le nœud Merge de n8n, en mode “Combine”, ne gère que 2 entrées fixes — quel que soit le nombre de branches câblées visuellement dessus. Câbler 4 ou 5 flux parallèles sur un Merge en mode Combine ne produit pas une erreur claire ; ça produit un comportement silencieusement incomplet.
La correction : passer le nœud Merge en mode “Append”, avec le nombre d’entrées explicitement défini (5, dans ce cas — 4 feeds + la corrélation temporelle décrite en section 6). En mode Append, chaque branche connectée à sa propre entrée est simplement concaténée à la sortie, sans tentative de “combinaison” par paire.
Mode : Append
Number of Inputs : 5
Résultat une fois corrigé : comportement stable et reproductible d’un run à l’autre, avec les 3-4 feeds systématiquement présents.
Pour une IP source détectée dans un signal, le rapport affiche désormais :
**CUSTOM DROP - r00ts3c ViewLog.asp DVR exploit attempt** — 1 occurrence(s)
- IP sources principales : 115.84.178.56 [Vietnam] (1)
* AbuseIPDB : score 87/100, 342 signalement(s)
* GreyNoise : malicious — exploiter
* OTX : 3 pulse(s) — DVR Botnet Wave 2026-Q2, SEA IoT Compromises
Une IP asiatique tentant un exploit DVR contre le WAN devient immédiatement lisible et vérifiable — pas juste “IP malveillante” affirmé sans preuve.
Le pipeline, même dans sa version enrichie, avait un angle mort structurel : chaque rapport est une photo indépendante des dernières 24h, sans mémoire des jours précédents. Une chaîne d’exploitation qui se construit lentement — reconnaissance légère un jour, scan plus ciblé quelques jours plus tard, tentative d’exploit ensuite — reste invisible tant que chaque événement pris isolément semble mineur.
C’est exactement ce que fait l’UEBA (User and Entity Behavior Analytics) des SIEM premium — mais il s’agit là de machine learning statistique classique établissant une baseline sur des semaines, pas de LLM génératif. Reproduire l’équivalent ne demande pas d’IA : une requête SQL sur l’historique des rapports déjà stockés suffit à couvrir le cas le plus courant.
SELECT date_generation, contenu
FROM resumes_soc
WHERE date_generation >= NOW() - INTERVAL '30 days'
ORDER BY date_generation DESC
LIMIT 60;
Pour chaque IP d’un signal du jour, le pipeline vérifie si elle apparaît dans les rapports des 30 derniers jours, et injecte le résultat dans le squelette :
* Historique : apparue dans 3 rapport(s) précédent(s) sur 30 jours
(il y a 1, 5, 12 jour(s)) — pattern potentiellement récurrent à surveiller.
Cette branche est câblée en parallèle des quatre feeds threat intelligence, sur le même nœud Merge (5 entrées au lieu de 4).
Une question revient systématiquement face à ce genre de projet : pourquoi ne pas utiliser un SIEM commercial ? La réponse chiffrée est plus instructive que l’intuition.
Sentinel et Splunk se facturent au volume de logs ingérés (GB/jour), pas au nombre d’utilisateurs — une nuance qui échappe souvent à la première estimation. Pour un environnement 50 users avec un stack correct (M365, EDR, réseau), le volume réaliste se situe autour de 40 GB/jour.
| Solution | Coût annuel réaliste (50 users, ~40 GB/jour) |
|---|---|
| UTMStack self-hosted (ce lab) | ~0 CHF de licence (matériel amorti) |
| Microsoft Sentinel, avec licences M365 E5 | ~50 000 à 70 000 USD/an |
| Microsoft Sentinel, sans exemption M365 | ~140 000 USD/an |
| Splunk Cloud + Enterprise Security | ~150 000 à 300 000 USD/an |
À ces montants s’ajoutent, dans tous les cas, un analyste dédié, une formation initiale et un budget d’intégration — portant le total réaliste à 150 000-400 000 CHF/an tout compris pour une vraie exploitation SIEM premium à cette échelle.
Microsoft Defender for Business (l’EDR endpoint inclus dans M365 Business Premium) fonctionne de façon totalement autonome, avec son propre portail (security.microsoft.com), sans nécessiter Sentinel. Sentinel est un SIEM qui agrège des sources au-delà de l’écosystème Microsoft (firewalls tiers, SaaS non-Microsoft, applications maison) et facture au volume — Microsoft a donc un intérêt commercial direct à entretenir l’impression qu’un Defender “complet” nécessite Sentinel, ce qui est techniquement faux pour la détection endpoint elle-même.
| Segment | Part estimée |
|---|---|
| Rien, ou antivirus natif seul | ~40% |
| EDR/XDR seul (Defender for Business, CrowdStrike…) | ~30% |
| EDR + MSSP externalisé (SIEM mutualisé, 3-8k CHF/mois) | ~20% |
| Vrai SOC interne avec SIEM premium dédié | ~5-10% |
Un lab UTMStack + pipeline IA local ne remplace pas un SIEM Enterprise pour une banque régulée. Il remplace en revanche, de façon économiquement et techniquement défendable, ce que font aujourd’hui la majorité des PME de 50 à 200 utilisateurs : rien, ou un EDR isolé sans corrélation réseau.
L’inférence sur CPU (i7-14700, 20 cœurs) donne des temps de génération de 2 minutes (Llama 8B) à 5-9 minutes (Qwen 14B) — largement viable pour un batch nocturne, moins confortable pour un usage interactif. Le point technique à connaître avant d’investir dans un GPU : un modèle qui déborde de la VRAM disponible perd la quasi-totalité du gain attendu, parce que l’inférence est séquentielle couche par couche et que chaque aller-retour entre GPU et RAM système via le bus PCIe coûte plus cher que le calcul lui-même. Un modèle à 85% en VRAM peut être aussi lent qu’un modèle 100% CPU.
Pour faire tenir Qwen 14B (Q4, ~10 Go) et Llama 8B (Q4, ~6 Go) confortablement en VRAM avec de la marge, 12 Go est le plancher pratique, 16 Go laisse de la marge pour tester des modèles plus gros (Mistral Small 24B, ~14 Go en Q4) sans dégradation.
Au-delà de Qwen 14B, le paysage des modèles ouverts spécialisés en raisonnement s’est beaucoup étoffé. Voici où se situe chaque option, avec des chiffres vérifiés plutôt que des ordres de grandeur approximatifs.
Tenables sur un lab domestique (CPU ou GPU consumer unique) :
| Modèle | Paramètres | Taille Q4 | GPU minimum | Contexte natif |
|---|---|---|---|---|
| DeepSeek-R1-Distill-Qwen-14B | 14,8B | 9,0 Go | 12 Go (RTX 4070) | 128K |
| Qwen3-14B (mode Thinking togglable) | 14,8B | 9,3 Go | 12 Go | 32K natif / 128K via YaRN |
| QwQ-32B | 32B | ~18-20 Go | RTX 3090/4090 (24 Go) | 32K |
| DeepSeek-R1-Distill-Qwen-32B | 32,8B | ~18-20 Go | RTX 3090/4090 (24 Go) | 128K |
| Llama 3.3 70B (dense, non-reasoning dédié) | 70B | ~40-43 Go | 48 Go (2× RTX 3090, ou Mac 64 Go) | 128K |
⚠️ Ne pas confondre avec le pipeline de production. Le pipeline de production tourne aujourd’hui sur Llama 3.1 8B (voir section 13), après avoir tourné sur Qwen 2.5 14B pendant toute la phase de construction documentée dans ce chapitre. Le Qwen3-14B et le Llama 3.3 listés ci-dessus appartiennent au panorama général des modèles de raisonnement disponibles en 2026 — ils n’ont jamais été testés sur ce pipeline précis, malgré la proximité de nom avec les modèles réellement utilisés (Qwen 2.5, Llama 3.1). Ce sont des générations différentes des mêmes familles, pas la même chose.
⚠️ Testé en conditions réelles sur ce lab : le R1-Distill-32B a été exécuté en CPU pur sur les 64 Go de RAM système de ce lab, partagés avec plusieurs VM actives. Le calcul théorique laissait ~25 Go de marge — en pratique, le chargement du modèle a occupé jusqu’à 98% de la RAM totale disponible avant même le début du calcul. Le run a fini par aboutir (29 minutes au total), mais sans aucune marge de sécurité réelle. Un seul processus supplémentaire réclamant de la mémoire au mauvais moment aurait pu faire échouer le calcul en cours de route. La marge théorique de ~25 Go ne s’est pas traduite par une marge pratique confortable.
Nécessitant un GPU de classe datacenter (hors de portée d’un lab domestique) :
| Modèle | Paramètres (actifs/total) | Taille | GPU minimum |
|---|---|---|---|
| Llama 4 Scout | 17B / 109B (MoE) | ~55 Go (INT4) | 1× H100 80 Go |
| Qwen3-30B-A3B (MoE) | 3B actifs / 30B total | ~17-20 Go | RTX 4090 24 Go — seule exception MoE tenable en local |
| Llama 4 Maverick | 17B / 400B (MoE) | ~200 Go (INT4) | 4× H100 |
| DeepSeek-R1 (modèle complet, pas le distillé) | 37B / 671B (MoE) | ~376 Go (Q4) | 8× H100/H200, ou config CPU-only spécialisée (voir plus bas) |
| DeepSeek V3.2 / V4 | 37B / 671B (MoE) | ~370-700 Go | 8× H200 |
| Qwen3-235B-A22B | 22B / 235B (MoE) | ~117-132 Go (INT4) | 4× H100 80 Go minimum |
| GLM-5.2 | 40B / 744B (MoE) | ~240 Go (2-bit) à 454-796 Go (Q4) | Multi-GPU datacenter uniquement |
| Kimi K2.5 | 32B / 1T (MoE) | ~550 Go | Cluster multi-nœuds |
⚠️ Le piège du “actif” vs “total” dans les modèles MoE. Un modèle Mixture-of-Experts comme Qwen3-235B-A22B n’active que 22 milliards de paramètres par token — mais la totalité des 235 milliards doit résider en VRAM, puisqu’on ne sait pas à l’avance quel “expert” sera sollicité. Le nombre de paramètres actifs décrit la vitesse d’inférence, pas le besoin mémoire.
Voie 1 — Achat de matériel dédié. Un rig 4× RTX 3090 d’occasion (sans NVLink) pour faire tourner DeepSeek-R1 671B complet en Q4 à ~4 tokens/seconde a été chiffré par la communauté à environ 2000 USD. Une alternative sans GPU du tout — un serveur bi-EPYC avec 384-512 Go de RAM DDR5 — tourne le même modèle en Q8 à 5-8 tokens/seconde, pour un budget de 2000 à 6000 USD selon le neuf/occasion.
Voie 2 — Location cloud à l’heure (tarifs vérifiés juillet 2026) :
| GPU | VRAM | Prix on-demand | Prix spot/marketplace |
|---|---|---|---|
| RTX 4090 (marketplace) | 24 Go | ~0,34 $/h | dès 0,14 $/h |
| A100 80 Go | 80 Go | 1,99–4,10 $/h | dès 0,60-0,68 $/h |
| H100 80 Go | 80 Go | 2,49–12,29 $/h (5× d’écart selon fournisseur) | dès 1,03-1,49 $/h |
| H200 141 Go | 141 Go | dès 0,50 $/h chez certains fournisseurs | — |
⚠️ Ces prix se périment vite, silencieusement. Le marché de la location GPU (surtout spot/marketplace) bouge sur des cycles de quelques semaines, pas d’années. Si tu lis ce chapitre plusieurs mois après juillet 2026, considère ce tableau comme un ordre de grandeur historique, pas un tarif actuel — revérifie directement chez le fournisseur avant toute décision budgétaire. Un peu ironique de le préciser dans un chapitre qui parle justement d’erreurs silencieuses, mais c’en est une : un tableau de prix qui semble à jour alors qu’il ne l’est plus.
ℹ️ Le piège du Pod loué pour un usage ponctuel. Louer un GPU façon RunPod/Vast.ai implique de déployer un Pod persistant, facturé tant qu’il tourne — pas seulement pendant le calcul réel. Pour un test ponctuel, deux alternatives évitent ce piège : le serverless GPU (facturation à la seconde de calcul, mise en veille automatique entre appels) ou l’API hébergée directe (Together AI, Fireworks AI proposent DeepSeek-R1 déjà déployé, facturé au token). Le calcul d’amortissement le confirme : pour un usage de 2h/jour, un GPU 24 Go loué coûte ~18 $/mois — il faudrait des années d’usage quotidien pour justifier l’achat d’une carte dédiée face à la location. L’achat matériel ne se justifie que pour un usage récurrent et intensif (un pipeline de production tournant quotidiennement), pas pour explorer ponctuellement un modèle plus gros.
Voie 3 — API hébergée, la plus simple : aucun matériel à gérer, facturation au token, quelques centimes par rapport généré. C’est la voie empruntée pour le test Claude Sonnet 5 de la section suivante. Le compromis reste la sortie de données hors du lab.
ℹ️ Mistral propose un plan gratuit substantiel pour tester. La “Experiment Plan” de leur API (La Plateforme) donne accès à tous leurs modèles, y compris Mistral Large, avec un plafond généreux de l’ordre d’1 milliard de tokens/mois — largement suffisant pour prototyper ce genre de pipeline sans dépenser un centime, et sans les questions de transfert hors UE qui se posent avec un fournisseur américain ou chinois (voir positionnement nLPD en section 7).
Sur une question croisant RAG (documentation du lab) et recherche web (SearXNG), un modèle 8B a produit une citation attribuant à la documentation du lab des informations qu’elle ne contenait pas — une IP jamais mentionnée nulle part dans les pages indexées, présentée comme documentée. La citation formelle (nom du fichier source) ne garantit pas que le contenu attribué soit réellement présent dans la source. À vérifier avant de faire confiance à une citation RAG sur un fait spécifique et récent.
Aujourd’hui, la base de connaissance Qdrant utilisée par Open WebUI (UTMStack Lab Docs) est peuplée manuellement : chaque page publiée sur ce site est ajoutée une par une via l’interface (Knowledge → Ajouter une page web). Ça fonctionne pour une dizaine de pages ; ça devient vite pénible sur les ~220 pages prévues à terme pour l’ensemble de cette documentation.
Deux architectures ont été évaluées pour automatiser cette réindexation, sans qu’aucune n’ait encore été implémentée :
Le déclenchement envisagé serait un nœud n8n en cron (hebdomadaire, par exemple), ou idéalement un webhook déclenché au moment du push sur ce dépôt.
⚠️ Ce qui ne doit pas aller dans le RAG — enseignement direct de ce projet : les rapports SOC générés quotidiennement (table
resumes_soc) ne sont volontairement pas candidats à l’indexation vectorielle. Une IP ou un nom de signature n’a pas de sémantique exploitable par une recherche par similarité, et des dizaines de rapports quasi identiques jour après jour noieraient le RAG de contenu répétitif. Ce type de donnée reste interrogeable directement en SQL (recherche par motif, par date) — le RAG est réservé à du contenu à vraie valeur sémantique : documentation, procédures, explications.
Ce chantier reste ouvert. Une fois implémenté, cette section sera mise à jour avec la marche à suivre complète.
L’intégration Office 365/Purview comme source d’alertes supplémentaire pour UTMStack est couverte dans le chapitre Intégrations — une fois ces logs actifs, la liste TERMES_SIGNAL de ce pipeline devra être enrichie en conséquence (voir la note à ce sujet plus haut dans cette page).
Tout ce chapitre repose sur un principe : sortir la classification du LLM parce qu’aucun modèle local testé ne la tenait de façon fiable. Une question naturelle se pose alors — un modèle réellement plus grand aurait-il seulement besoin de cette béquille ? Pour y répondre, le même jeu de données brutes (agrégations OpenSearch + rule_description, sans aucun tri déterministe ni squelette) a été soumis à quatre configurations, hors du pipeline de production, dans des workflows n8n isolés dédiés à ce seul test.
Un prompt unique demandait au modèle de tout faire lui-même : classer les signaux prioritaires, identifier ce qui est neutralisé, repérer le bruit, signaler les règles à fort volume suspectes de faux positif, et conclure sur un nombre précis de signaux réels — exactement la tâche que le tri déterministe du reste du pipeline effectue aujourd’hui en JavaScript.
⚠️ À lire avant le tableau, pas seulement après. La plupart des configurations ci-dessous reposent sur un seul ou deux runs, pas sur une série statistique. Un tableau comparatif a tendance à faire paraître ses verdicts plus établis qu’ils ne le sont — c’est une leçon que ce chapitre applique à ses propres modèles testés, elle s’applique tout autant à ses propres conclusions. Traiter chaque ligne comme un indice, pas comme un verdict scellé.
| Modèle | Temps total | Corrélation croisée source/cible | Cohérence interne | Verdict |
|---|---|---|---|---|
| Claude Sonnet 5 (run 1) | Quelques secondes (API) | ⚠️ Construite mais fausse — 192.168.1.203 identifié comme “cible ET source”, présenté comme un pivot (voir correction ci-dessous) |
✅ Aucune contradiction | Rapport bien écrit, mais avec une erreur de fond non détectée à l’origine |
| Claude Sonnet 5 (run 2) | Quelques secondes (API) | ⚠️ Même erreur reproduite indépendamment | ✅ Conclusion cohérente avec le corps du texte | Reproductible sur deux runs — l’erreur aussi |
| Mistral Large (API, société UE) | Quelques secondes (API) | ⚠️ Même erreur — même hôte présenté comme source ET cible, recommandation d’isolement basée sur une fausse prémisse | ✅ Aucune contradiction, format tableau clair | Qualité de rédaction égale ou supérieure à Sonnet, mais sujette à la même erreur de fond |
| DeepSeek-R1-Distill-14B (raisonnement bypassé) | 11 min | ❌ Non — chaque signature traitée isolément | ⚠️ Un même type d’anomalie TCP classé différemment selon l’endroit du texte | Structure correcte, calibration incohérente |
| DeepSeek-R1-Distill-14B (raisonnement forcé) | 15 min 33s | ❌ Non | ❌ Contradiction franche : 211 vs 207 alertes annoncées ; mélange de langue (un mot chinois en plein milieu du texte français) ; “215 signaux prioritaires” annoncés en fin de rapport, noyant les 2 vrais signaux dans un bruit d’alarme massif | Dégradé par rapport au run sans raisonnement forcé |
| DeepSeek-R1-Distill-32B (raisonnement forcé) | 29 min 10s | ❌ Non — les faits sont listés côte à côte sans être reliés | ⚠️ Léger : “5 signaux” annoncés en conclusion, 4 seulement énumérés | Nettement meilleure calibration que le 14B, toujours pas de corrélation multi-signaux |
⚠️ Correction post-publication. La ligne “corrélation croisée” pour Sonnet et Mistral a été corrigée après la rédaction initiale de ce chapitre — voir la section qui suit pour l’explication complète. En résumé :
192.168.1.203est l’IP WAN d’OPNsense (l’adresse de sortie NAT du lab), pas un poste identifiable. Tout trafic sortant du LAN apparaît avec cette IP — ce n’est jamais un hôte compromis unique qu’on peut isoler. Les deux modèles ont construit un scénario de pivot cohérent et bien écrit à partir de cette IP, mais factuellement infondé.
La version initiale de cette section affirmait que Sonnet et Mistral avaient réussi une corrélation que R1 ratait systématiquement : identifier 192.168.1.203 comme à la fois cible de scans entrants et source d’exploitation sortante, concluant à un pivot. C’était le résultat le plus mis en avant de tout ce chapitre — jusqu’à ce qu’une relecture, faite lors de l’usage réel du pipeline en production (section 10), révèle le problème : cette IP est la passerelle NAT du lab, pas un hôte. Le “scénario de pivot” était une confabulation plausible, pas une corrélation réelle — les deux modèles ont vu la même IP des deux côtés d’un flux et en ont déduit, à tort, qu’il s’agissait d’une seule machine compromise.
Ce n’est pas la même erreur que le cas Golden Ticket décrit plus loin (une donnée mal interprétée) : ici, la donnée était lue correctement, mais le prompt ne contenait aucune information sur la topologie réseau du lab. Aucun des modèles ne pouvait savoir que cette IP précise avait un statut particulier — l’erreur ne vient donc pas d’un défaut de raisonnement, mais d’un trou de contexte identique pour tous les modèles testés (R1 n’a simplement pas fait le lien du tout, ce qui l’a paradoxalement empêché de se tromper de cette façon précise).
Ce qui reste vrai, une fois cette correction faite : Sonnet et Mistral ont produit des rapports mieux structurés, plus lisibles, et sans contradiction interne que les deux versions de R1 testées. Ce qui ne tient plus : l’idée qu’ils aient démontré une capacité de corrélation multi-signaux supérieure sur ce jeu de données précis. Le vrai différenciateur observé, honnêtement, se limite à la qualité de rédaction et à l’absence de contradiction interne — pas à une corrélation causale correcte.
⚠️ Particularités techniques rencontrées lors de ces tests, pour qui voudrait les reproduire. Les modèles R1 ont une tendance documentée par DeepSeek eux-mêmes à “bypasser” leur propre mode de raisonnement sur certaines requêtes (aucune balise
<think>produite), rendant le temps d’exécution imprévisible d’un run à l’autre. Le forcer demande d’appeler l’API Ollama en moderawavec les tokens spéciaux exacts du modèle (<|User|>,<|Assistant|>— des barres verticales pleine chasse, pas des barres standards) et de pré-remplir la réponse avec<think>\n. Le raisonnement interne produit dans ce mode reste très souvent en anglais, indépendamment de la langue du prompt — sans incidence sur la langue de la réponse finale, qui suit correctement la consigne.
Sonnet a répondu en quelques secondes pour quelques centimes via API. Le R1-32B a nécessité 29 minutes en CPU pur, sur une machine dont la RAM a frôlé la saturation (98% dès le chargement, sur 64 Go partagés avec d’autres VM) — pour une qualité de rédaction et une cohérence interne inférieures à Sonnet, une fois la question de la corrélation NAT mise de côté (voir correction ci-dessus). Loué sur un GPU 24 Go à l’heure (~0,30 $/h), le même modèle aurait probablement répondu en quelques minutes pour quelques centimes également — mais l’écart de qualité rédactionnelle avec Sonnet, lui, ne se comble pas en changeant de matériel.
Une objection légitime se pose à ce stade : si le but final est un rapport lu par un analyste humain, est-ce qu’une éventuelle erreur du LLM ne serait pas simplement repérée à la lecture, rendant le déterminisme superflu ?
La réponse dépend du type d’erreur, et c’est la distinction la plus importante de ce chapitre :
Le déterminisme ne protège donc pas contre “le LLM se trompe” en général — il protège spécifiquement contre le cas où le LLM se trompe sans que rien ne le signale. C’est ce risque précis, pas la qualité générale du modèle, qui justifie de garder une classification déterministe en frontal d’un pipeline qui tourne sans supervision immédiate, même si un modèle de la classe de Sonnet serait probablement plus fiable qu’un humain fatigué sur la majorité des cas.
ℹ️ Ce que ce test ne prouve pas. Deux runs Sonnet concordants sont un bon indice de fiabilité, pas une preuve d’infaillibilité — la même prudence méthodologique que celle appliquée tout au long de ce chapitre aux modèles locaux s’applique ici aussi. Pour un usage supervisé où l’analyste relit chaque rapport, un grand LLM sans contrainte déterministe est probablement le meilleur choix. Pour un pipeline entièrement automatisé sans lecture systématique, le déterminisme reste la police d’assurance qu’aucun modèle, aussi capable soit-il, ne remplace encore totalement.
Confirmation par re-test. Pour vérifier que cette erreur venait bien d’un trou de contexte et non d’un défaut de raisonnement, le même prompt a été relancé sur Claude Sonnet avec une seule ligne ajoutée : “192.168.1.203 est l’IP WAN d’OPNsense (passerelle NAT) — ne jamais construire de scénario de pivot sur cette seule base.” Résultat : le modèle a non seulement évité l’erreur, mais a explicitement noté chaque occurrence de cette IP avec la mention (NAT), et a reformulé sa recommandation en conséquence — au lieu d’affirmer qu’un hôte précis était compromis, il a écrit “identifier l’hôte LAN réel à l’origine (logs NAT/DHCP, table de connexions au moment des timestamps)”. C’est la bonne posture épistémique : signaler l’incertitude plutôt que la combler par une inférence plausible mais non fondée. Ce même run a aussi retrouvé, sans qu’on le lui demande, le diagnostic du bucket High level Suricata alert comme faux positif de catégorisation à 99% de son volume — le même réflexe de méta-critique du dataset déjà observé chez Mistral sur la boucle SMTP (section 3). Ce contrôle confirme que l’erreur de corrélation NAT était bien un trou de contexte réparable, contrairement au cas Golden Ticket ci-dessous, qui reste une erreur d’interprétation même une fois toutes les données disponibles.
Les tests initiaux ont révélé plusieurs types d’erreurs : l’invention pure (une CVE non vérifiable, corrigée par une interdiction explicite dans le prompt), l’incohérence de calibration (un signal minimisé ou un décompte contradictoire), et la confabulation sur un trou de contexte (la fausse corrélation NAT décrite plus haut). L’usage en production de la variante cloud (section 10) en a révélé un quatrième, tout aussi insidieux, sur Mistral pourtant jugé le plus fiable des modèles testés.
Une alerte réelle nommée Circuit Breaker: Golden Ticket Attack Detection est apparue dans les données. Son contenu véritable, vérifié directement dans OpenSearch, n’a rien à voir avec une attaque Kerberos : c’est UTMStack qui signale que sa propre règle de corrélation “Golden Ticket Attack Detection” a planté et a été désactivée après 5 échecs ("expression value cannot be nil after placeholder resolution") — un message de plomberie interne, pas une détection de sécurité.
Mistral a lu le nom littéralement et construit, avec un aplomb total et sur deux runs indépendants, une analyse Kerberos complète et plausible : vérification des tickets de plus de 10h, audit des comptes privilégiés, réinitialisation du compte krbtgt. Rien dans le ton n’indiquait une incertitude — la même prose confiante que pour les signaux réellement confirmés.
Ce cas est distinct de l’invention pure : le nom de la règle était authentique, présent dans les vraies données. L’erreur porte sur l’interprétation du sens, pas sur l’existence du fait. Une règle anti-invention (“ne cite jamais une référence absente des données”) ne suffit pas à s’en prémunir, puisque rien n’a été inventé au sens strict. Le même run a produit une seconde illustration, plus grossière : une mention d’alerte SOC Pipeline Heartbeat Failure — hors service depuis 26h, alors que le pipeline venait de fonctionner pour produire ce rapport même — une contradiction logique interne qu’un simple garde-fou de cohérence temporelle pourrait détecter, mais qui n’existe pas dans ce prompt d’analyse libre.
Le point à retenir : même le modèle le plus fiable de la comparaison reste sujet à ce type d’erreur, reproductible sur plusieurs runs — et ce chapitre en documente maintenant deux exemples concrets et distincts (la corrélation NAT plus haut, Golden Ticket ici), pas un cas isolé. C’est un argument supplémentaire, indépendant de la qualité du modèle, en faveur de la structure abordée en section 10 — garder le pipeline déterministe comme rapport de référence, et traiter toute analyse libre comme un second avis à vérifier, jamais comme une source unique de vérité.
Les tests de la section précédente ont mis en évidence un résultat qu’il serait malhonnête de taire : Mistral Large, hébergé en UE, égale ou dépasse la qualité d’analyse du meilleur modèle testé, pour un coût mensuel négligeable. Cette section documente comment cette option a été intégrée au pipeline, en complément du pipeline local existant, jamais à sa place.
L’architecture déterministe décrite dans ce chapitre (tri JS, squelette pré-rédigé, enrichissement threat intelligence, corrélation temporelle) reste strictement identique. Le seul changement : le nœud d’appel LLM, qui pointait vers Ollama/Qwen en local, pointe désormais vers l’API Mistral. Le modèle ne fait toujours que de la complétion — remplir des emplacements [COMMENTAIRE_N] dans un texte déjà classé et structuré, jamais de classification libre. Toutes les garanties du pipeline déterministe restent intactes ; seule la qualité de rédaction change.
Pour un rapport complet (prompt d’environ 3000 tokens en entrée, réponse de 1000-1500 tokens), le coût avec Mistral Large (0,50 $ / 1,50 $ par million de tokens) revient à :
Un rapport par jour, 30 jours : ≈ 0,11 $/mois
Rapport quotidien + à la demande quotidien : ≈ 0,22 $/mois
Négligeable, dans le même ordre de grandeur que Claude Sonnet testé en section 9.
⚠️ Le tier gratuit n’est pas fait pour la production. Mistral propose un plan “Experiment” gratuit (~1 milliard de tokens/mois), largement suffisant en volume pour ce cas d’usage — mais ce tier utilise les données envoyées pour l’entraînement de leurs modèles par défaut, sauf opt-out manuel dans Admin Console → Confidentialité. Pour un usage réel, même de lab, le passage sur le Scale plan (pay-as-you-go, sans minimum, sans abonnement) est la seule option sérieuse : il active la garantie de non-entraînement par défaut, et un DPA (Data Processing Addendum) standard est disponible en libre accès sur
legal.mistral.ai, sans négociation commerciale ni contact avec les ventes — contrairement à un accord “Enterprise” sur-mesure. Vérifier dans la console si une action explicite (acceptation du document) est nécessaire pour qu’il s’applique formellement au compte. Le surcoût pour accéder au Scale plan est nul — c’est une facturation à l’usage, pas un forfait.
Le passage Suisse → UE est reconnu comme offrant un niveau de protection adéquat par la nLPD, sans clause contractuelle type ni garantie supplémentaire — contrairement à un transfert vers les USA ou tout pays sans décision d’adéquation. Combiné au DPA self-serve disponible sur le Scale plan et à la garantie de non-entraînement, ce choix règle la question du mécanisme de transfert.
Il ne règle pas, à lui seul :
Ces points relèvent d’une vérification par un DPO ou un juriste, indépendamment du fournisseur choisi — le choix de Mistral simplifie le terrain, il ne dispense pas de l’analyse.
Plutôt que de choisir entre “garder le déterminisme” et “profiter de la qualité d’analyse libre d’un grand LLM”, le pipeline de production fait tourner les deux, en parallèle, jamais mélangés :
| Rapport officiel | Rapport complémentaire | |
|---|---|---|
| Architecture | Tri déterministe + squelette + complétion | Données brutes, analyse libre |
| Fiabilité | Garantie par construction | Dépendante de la qualité du modèle, non garantie |
| Stockage | Table resumes_soc |
Table séparée resumes_soc_libre |
| Étiquetage | Aucun avertissement nécessaire | Bandeau explicite “à titre indicatif”, objet de mail préfixé, page web sur fond distinct |
| Rôle | Source de vérité, alimente le heartbeat | Second avis, peut repérer ce que TERMES_SIGNAL ne couvre pas encore |
Le rapport complémentaire a une vraie valeur ajoutée démontrée : sur un run réel, il a correctement identifié la boucle SMTP EHLO comme faux positif probable, sans qu’on le lui indique — exactement le type de découverte qu’une liste de mots-clés figée ne peut pas anticiper. Mais le cas “Golden Ticket” documenté plus haut montre aussi sa limite : sans supervision, il ne remplace pas le rapport officiel, il l’enrichit.
| Besoin | Recommandation |
|---|---|
| Pipeline automatisé, aucune sortie de donnée acceptable | 100% local (Llama 3.1 8B en production, historiquement Qwen), comme documenté dans ce chapitre |
| Rapport officiel avec une meilleure qualité de rédaction, coût acceptable | Variante cloud (Mistral) en remplacement du moteur, architecture déterministe inchangée |
| Second avis d’analyse plus riche, en complément | Pattern double-rapport, jamais en source unique |
| Contexte réglementaire strict (secteur régulé, contrat client zéro-sortie) | 100% local, indépendamment du coût de la variante cloud |
La configuration de déploiement complète de cette variante (comptes, clés, credential SMTP, fichiers JSON) est détaillée dans l’annexe technique.
Un pipeline SOC qui échoue en silence est un problème de conception, pas un détail. Si n8n s’arrête, si Ollama plante, ou si le Schedule Trigger reste dépublié après un import — le rapport quotidien ne s’affiche simplement plus. Pour un analyste qui consulte le dashboard, l’absence de rapport ressemble en tout point à “rien à signaler”. C’est le pire mode de défaillance possible pour un outil de sécurité : la panne produit exactement le même signal que l’absence d’incident.
Le réflexe naturel serait d’ajouter un workflow n8n de plus, qui vérifie que les autres tournent bien. C’est un contresens : un surveillant qui meurt en même temps que le système surveillé ne détecte rien. Si n8n est down, un workflow de heartbeat hébergé dans n8n est down aussi.
Le heartbeat doit tourner en dehors du système qu’il surveille. Dans ce lab, il tourne directement sur la VM UTMStack — celle qui reste debout même si docker-services (n8n, PostgreSQL, Qdrant) est éteinte.
Un script shell interroge en local la table resumes_soc :
SELECT COALESCE(ROUND(EXTRACT(EPOCH FROM (NOW() - MAX(date_generation)))/3600), 9999)
FROM resumes_soc;
Si le dernier rapport a plus de 26h (2h de tolérance après le run planifié de 6h00), le script émet un message syslog vers l’agent UTMStack — transformant la panne du pipeline en un événement que le SIEM peut lui-même corréler et afficher comme une alerte.
┌─────────────────────────┐
│ VM UTMStack │ tourne indépendamment
│ (timer systemd, 08h00) │ de docker-services
│ │
│ soc-pipeline-heartbeat.sh │
│ → interroge resumes_soc │
│ en local (docker exec) │
│ → si > 26h : syslog │
└─────────────┬───────────────
│ logger -n <agent> -P 7014
▼
┌─────────────────────────┐
│ Agent Windows UTMStack │
└─────────────┬───────────────
▼
┌─────────────────────────┐
│ v11-log-syslog-* │ message brut visible
└─────────────┬───────────────
▼
┌─────────────────────────┐
│ Règle de corrélation │ contains("raw",
│ UTMStack (créée via UI) │ "SOC-PIPELINE-HEARTBEAT
│ Confidentiality/Integrity/ │ FAILURE")
│ Availability = 3/3/3 │
└─────────────┬───────────────
▼
┌─────────────────────────┐
│ v11-alert-* │ alerte High,
│ "SOC Pipeline Heartbeat │ visible au dashboard
│ Failure" │
└─────────────────────────┘
Fail-safe, pas fail-silent : toute réponse anormale de la requête PostgreSQL (base injoignable, table vide, sortie corrompue) est traitée comme un âge de “9999h” — donc comme une panne à signaler, jamais comme un cas neutre passé sous silence.
Un garde-fou contre les faux positifs : la VM UTMStack de ce lab n’étant pas allumée en continu, un simple redémarrage produirait un déclenchement immédiat (le dernier rapport a alors mécaniquement plus de 26h). Le script vérifie l’uptime de la machine et diffère la vérification si elle vient de démarrer depuis moins de 2h — sans jamais annuler une vraie alerte, seulement la reporter à la vérification suivante.
Déploiement en timer systemd plutôt qu’en cron classique, pour deux raisons concrètes : Persistent=true rattrape l’exécution manquée si la VM était éteinte à l’heure planifiée — indispensable pour un lab qui ne tourne pas 24/7 — et journalctl -u soc-pipeline-heartbeat centralise le diagnostic au même endroit que le reste des services systemd déjà en place sur cette VM.
Le mécanisme complet a été testé de bout en bout, alerte forcée à l’appui : script → syslog → agent → OpenSearch → règle de corrélation → alerte visible au dashboard, avec sévérité High et statut Open. Le script et sa procédure de déploiement complète (y compris la construction de la règle de corrélation via l’interface UTMStack, faute de format .yaml sur cette plateforme — la corrélation vit en base PostgreSQL, dans un langage d’expression propre à UTMStack) sont détaillés dans l’annexe technique.
TERMES_SIGNAL/BRUIT/CONTROLE — une liste de mots-clés maintenue à la main, qui doit être enrichie à chaque nouvelle source de données (Kali red team, O365, Azure) sous peine de laisser passer des signatures inconnues en zone grisev11-log-*, canal Microsoft-Windows-Windows Defender/Operational), mais aucune règle de corrélation UTMStack native ne promeut cet événement en alerte (v11-alert-*) — un chantier de règle de corrélation dédiée reste à faire147.185.132.63, bloqué en entrée par une règle DShield et cible d’un trafic sortant du LAN sur une autre règle) n’a été repéré que par une analyse libre complémentaire, jamais par le tri déterministe — chantier différé, voir ci-dessousUne revue d’architecture menée sur ce pipeline a identifié plusieurs améliorations supplémentaires, sciemment reportées plutôt qu’appliquées dans l’immédiat :
TERMES_SIGNAL reste visible dans les données du rapport, mais pas mise en avant dans la section prioritaire — l’analyste doit la repérer lui-même. Corriger ça proprement demande de redistribuer la construction du squelette entre deux nœuds du pipeline (aujourd’hui, le squelette est écrit avant que les données threat intelligence soient disponibles) — une opération qui touche deux nœuds actuellement stables. Reportée à une session de durcissement dédiée, après les intégrations d’agents et les tests Kali, pour ne pas fragiliser un pipeline qui vient d’être stabilisé juste avant une phase de tests plus large.Cette section documente un changement survenu après la stabilisation complète du pipeline racontée dans les sections précédentes — un test concret, mené sur quelques jours d’usage réel avec un volume d’alertes plus élevé qu’aux tests initiaux, a révélé une limite de Qwen que les tests initiaux (section 3) n’avaient pas fait apparaître, faute de volume suffisant à l’époque.
Sur des rapports à fort volume de signatures peu prioritaires (la section “Signatures non classées”, listant les raretés suspectes en fin de squelette), Qwen 2.5 14B a montré une tendance reproductible à recycler mot pour mot le même commentaire générique au-delà des 5-6 premiers éléments :
Le trafic est suspect et mérite une investigation approfondie pour confirmer sa nature malveillante.
Cette phrase, ou une variante très proche, est réapparue à l’identique sur 9 signatures distinctes (nœuds Tor, scan Zmap, réponse MZ depuis une IP nue, sonde gitrepo, scanner LeakIX) dans un même rapport — sans jamais mentionner l’IP, le pays, ou la particularité propre à chaque signature.
Un premier réflexe a été de suspecter une troncature de la fenêtre de contexte (le symptôme documenté en section 4 de l’annexe de déploiement). Vérification faite via le champ prompt_eval_count retourné par Ollama : le prompt consommait 3600-3900 tokens sur les runs concernés, loin sous la limite de 8192 configurée. Ce n’est donc pas un problème de contexte.
Deux corrections de prompt ont été testées avant de conclure à une limite du modèle :
Ni l’augmentation du contexte, ni le renforcement du prompt n’ont corrigé la dérive. C’est une limite de tenue d’attention en fin de génération longue, propre à cette classe de modèle sur cette tâche précise — pas un problème de formulation ou de mémoire disponible.
Le principe déjà énoncé en section 3 — “sur une tâche de pure complétion, le modèle le plus docile bat parfois le plus capable” — a motivé un nouveau test sur utmstack-analyst-test (Llama 3.1 8B, déjà construit depuis le comparatif initial, aucune infrastructure supplémentaire nécessaire). Sur le même jeu de données ayant produit la répétition chez Qwen, Llama a rédigé un commentaire distinct et spécifique pour chacune des mêmes 9 signatures — nœud Tor identifié comme tentative d’anonymisation, réponse MZ décrite comme technique de distribution de malware via IP nue, sonde gitrepo comme risque d’exfiltration de code source — sans aucune répétition, et avec un temps de génération plus court (8B contre 14B).
| Critère | Qwen 2.5 14B | Llama 3.1 8B |
|---|---|---|
| Richesse de contexte (signaux prioritaires) | Plus riche, plus narratif | Plus sobre, factuel |
| Tenue de consigne en fin de rapport long | Dégrade en commentaire générique recyclé au-delà de ~6 items | Reste spécifique jusqu’au bout |
| Respect de la balise de remplacement | Parfois laissée visible à côté du commentaire | Idem sans l’instruction de remplacement ; corrigé par la même instruction que Qwen |
| Temps de génération (CPU, ce lab) | Plus long (14B) | Plus court (8B) |
Le pipeline de production (rapport quotidien et à la demande) tourne désormais sur Llama 3.1 8B (utmstack-analyst-test dans Ollama). La justification tient en une phrase, directement liée au principe déjà posé en section 9 sur les erreurs silencieuses : un rapport qui répète la même phrase sur plusieurs signatures en fin de liste entraîne exactement le réflexe de fatigue d’alerte que ce chapitre met en garde contre ailleurs — un format qui a l’air normal 99% du temps, jusqu’au jour où une des raretés suspectes est réellement différente et se noie dans le pattern habituel. Ce n’est pas une erreur factuelle (chaque commentaire pris isolément reste correct), mais un risque réel sur la durée pour un outil dont la valeur repose sur la confiance répétée jour après jour.
Qwen reste documenté comme référence historique dans les sections 2 à 9 de ce chapitre — c’est le modèle qui a réellement servi à construire et stabiliser cette architecture sur 12 versions, et le récit reste fidèle à ce qui a été testé. La bascule vers Llama ne remet rien en cause de cette histoire ; elle en est la suite logique, une fois un nouveau type de limite découvert en conditions réelles de production, sur un volume que les tests initiaux n’avaient pas couvert.
Une recherche des nouveautés 2026 a fait ressortir deux candidats alternatifs, non retenus faute de nécessité une fois Llama validé :
utmstack-resume-quotidien-llama.json et utmstack-resume-a-la-demande-llama.json remplacent les fichiers -v12.json (Qwen) comme référence de production — ces derniers restent publiés et documentés comme trace historique du parcours décrit dans ce chapitre, pas retirés du dépôt. Aucune configuration supplémentaire requise au-delà du changement de nom de modèle : le Modelfile de utmstack-analyst-test existe depuis le comparatif initial (section 3), voir l’annexe de déploiement.
← Retour à l’index → Guide de déploiement du pipeline