asp-forge (3/3) — Leçons d'un SOC agentique en lab : ce qui surprend, ce qu'on garde
Troisième et dernier volet de la série asp-forge. Après l’architecture (article 1) et le pipeline cascade (article 2), il reste la question qui intéresse vraiment l’ingénieur : qu’est-ce qui s’est mal passé, qu’est-ce qui a surpris, et qu’est-ce qu’on garde pour la suite ? Pas de success story édulcorée — on partage les pièges réels.
Bug LoRA dynamique : une discipline de version pinning
Premier piège qui a coûté plusieurs jours de débogage. Le support des
LoRA dynamiques sur llama.cpp est une fonctionnalité récente
(stabilisée fin 2024 via la PR #10994), et certaines builds
intermédiaires souffrent d’un comportement subtil : le flag censé
charger les adaptateurs sans les appliquer immédiatement
(--lora-init-without-apply) est silencieusement ignoré.
Le résultat est insidieux : on lance llama-server avec trois
adaptateurs, on envoie une requête sans lora, et au lieu d’obtenir le
comportement du modèle de base, on obtient un mix imprévisible parce
que le premier adaptateur a été appliqué globalement au démarrage.
L’agent OPNsense répond avec le ton de l’agent WireGuard. Ou pire : le
test fonctionnel passe (la sortie est plausible), mais la décision est
biaisée par un LoRA qui ne correspond pas au domaine de l’alerte.
La leçon est simple : pinner la build llama.cpp, et tester que le comportement est bien isolé par adaptateur. Concrètement, un test de non-régression au démarrage qui envoie une requête identique avec chaque adaptateur (ou sans), et compare les sorties pour vérifier qu’elles divergent comme attendu. Sans ce test, on ne détecte le bug qu’en production, sur une décision de blocage anormale.
Auto-block avant analyse : choix opérationnel défendable
L’article précédent décrit un choix qui paraît contre-intuitif : pour les alertes au-dessus d’un certain niveau Wazuh, on bloque l’IP immédiatement, avant que le pipeline LLM n’ait raisonné. Le LLM décide ensuite si la règle reste ou si elle est levée.
Ce choix a été contesté, puis adopté, puis confirmé empiriquement. L’argument :
- la latence cumulée du pipeline (triage + SOC + éventuelle contre-analyse CERT) est de l’ordre de 20 à 30 secondes ;
- une attaque par brute force agressive enchaîne plusieurs dizaines de tentatives par minute ;
- attendre 30 secondes pour bloquer = laisser passer un credential stuffing partiel.
Le compromis : bloquer d’abord, raisonner ensuite. Un faux positif
court (l’IP d’un client légitime bloquée 30 secondes) est beaucoup
moins grave qu’une intrusion réelle. La condition nécessaire pour que
ce compromis soit acceptable, c’est la réversibilité totale : chaque
règle a un UUID, chaque blocage est journalisé, et le LLM peut
explicitement décider delete_rule(uuid) après son analyse.
Sans la couche de réversibilité, ce choix serait imprudent. Avec elle, il devient simplement la politique opérationnelle la plus conservative.
Tester sans attendre une vraie attaque : l’injection d’alertes
Une question revient toujours quand on opère ce type de pipeline : comment vérifier qu’il fonctionne quand le lab n’est pas en train de subir une intrusion réelle ? Attendre qu’un scanner Internet tombe sur le honeypot pour valider la chaîne est inopérant : le délai est imprévisible, et l’événement obtenu rarement reproductible.
La discipline retenue est l’injection contrôlée d’alertes. On fabrique des événements au format Wazuh natif (le JSON sortant du manager) et on les pousse, soit directement dans le stream Redis du forwarder, soit en simulant un webhook Wazuh en amont. Trois familles de tests systématiques :
- Test du seuil de niveau. Wazuh classe ses alertes par level (1 à 15). Le pipeline ne traite que celles au-dessus d’un seuil configuré (typiquement ≥ 5). On envoie des fakes aux niveaux 3, 5, 10 et 13 et on vérifie : (1) que les niveaux trop bas sont droppés silencieusement par le forwarder ; (2) que les niveaux ≥ seuil sont bien injectés dans le bon stream Redis ; (3) que les niveaux ≥ 13 déclenchent le shortcut Telegram avant même le pipeline.
- Test du chemin par règle. Pour chaque rule group qu’on veut
supporter (
authentication_failed,web,syscheck,rootcheck), on a un payload de référence sauvegardé sur disque qui correspond à une vraie alerte capturée passée. On le rejoue via un scriptinject-alert <rule_group>et on observe : routage vers le bon contexte LoRA, décision SOC dans la whitelist attendue, sortie finale conforme. - Test des bypass de pipeline. Volontairement injecter des
payloads malformés (champ
data.srcipabsent, decoder inconnu) pour vérifier que laValidationErrorPydantic en frontière déclenche la boucle de correction locale plutôt que de planter le graphe.
Concrètement, ces tests vivent comme un script CLI dédié, exécuté manuellement avant chaque modification de prompt, de seuil ou de LoRA, et automatiquement en CI sur le repo de configuration. C’est l’équivalent agentique d’un harness d’intégration : on ne déploie pas une nouvelle politique de filtrage en production sans avoir revalidé que les niveaux 13 atteignent bien Telegram et que les niveaux 4 sont bien droppés.
Bénéfice opérationnel découvert au passage : ces injections sont aussi un excellent outil de formation. Quand un nouveau collaborateur prend en main le système, on lui injecte une dizaine d’alertes types et on lui demande d’expliquer pourquoi le pipeline a tranché tel ou tel verdict. C’est plus pédagogique que de lire un schéma — il observe le système réagir en direct.
L’agent CERT justifie-t-il son coût ?
C’est la question la plus honnête de la série. Cette deuxième passe de raisonnement par un agent CERT indépendant qui challenge la décision du SOC coûte un appel LLM supplémentaire, soit 10 à 15 secondes de latence et un peu de CPU. Est-ce qu’on en tire vraiment quelque chose ?
Données observées sur le sous-ensemble d’incidents où le CERT a été déclenché (typiquement les blocages > 24h ou sur des assets critiques) :
- ~75 %
confirm— le CERT arrive à la même conclusion que le SOC ; - ~15 %
refute— il arrive à une conclusion différente ; - ~10 %
uncertain— il ne tranche pas seul.
À première lecture, on pourrait se dire que les 75 % de confirm
sont du gâchis. Ce serait une erreur. Ces 75 % constituent une
signature d’audit : on peut prouver, lors d’une revue de
conformité, que la décision a été prise par deux raisonnements
indépendants. C’est précieux dès qu’on touche à un cadre réglementé.
Les 15 % de refute sont la valeur opérationnelle directe : ce sont
des incidents que le SOC seul aurait fait passer (ou bloquer) à tort.
Le CERT les attrape. Sur un volume conséquent, ce 15 % se traduit par
des dizaines de décisions incorrectes évitées par mois.
Conclusion empirique : la contre-analyse CERT n’est pas du théâtre, mais il faut être discipliné sur le quand. La déclencher systématiquement serait du gâchis. La réserver aux actions à fort impact est rentable. La politique d’invocation est plus importante que le modèle lui-même.
Le rebouclage CERT → OPNsense : la boucle qui valide le LoRA
Quand le CERT statue refute ou que la revue humaine décide qu’un
blocage initial était infondé, l’IP est dite « blanchie » et la
règle firewall doit être levée. Concrètement, cette levée n’est pas
un appel d’API direct codé en dur — c’est l’agent OPNsense
lui-même qui est re-sollicité avec un contexte différent : non plus
“propose un blocage”, mais “produis le payload delete_rule pour
l’uuid historisé”.
Ce rebouclage n’est pas anecdotique. Il a une conséquence pratique décisive : le LoRA OPNsense est sollicité aussi bien à la pose qu’à la levée d’une règle. Le coût d’entraînement du LoRA — qui se compte en jours de travail d’extraction et de formatage de la connaissance métier — n’est pas amorti par l’usage à la pose seul. Il est amorti sur le cycle complet : block → analyse → contre-analyse → unblock.
C’est aussi ce qui transforme l’agent OPNsense en banc de validation
permanent du LoRA. Une régression introduite par un nouveau jeu
d’entraînement (champ mal orthographié, mauvais format d’uuid, oubli
d’un paramètre devenu obligatoire dans une version OPNsense récente)
sera détectée immédiatement par échec d’API sur le delete_rule réel,
pas dans un benchmark synthétique. Le pipeline est le benchmark, on
l’a évoqué dans l’article 2 — le cycle de levée le confirme à pleine
charge.
La leçon généralisable : un LoRA spécialisé n’a de valeur que dans une boucle d’usage qui couvre l’ensemble de son périmètre métier. Si on ne sollicite l’agent que pour les actions positives (poser une règle), on ne valide qu’une moitié du LoRA. Le rebouclage par le CERT ferme cette boucle proprement.
Performance CPU : ce qu’on mesure vraiment
Les benchmarks publics des SLM ne reflètent pas l’usage SOC. On y voit souvent des chiffres à 100+ tokens/s sur Qwen 3B en CPU, mais c’est sur des contextes courts et sans serialisation JSON contrainte. En conditions réelles SOC, le contexte porte 1 500 à 3 000 tokens (alerte
- historique + contexte CTI), la sortie est forcée en JSON Schema, et on observe :
- 15 à 25 tokens/s en génération sur Qwen 2.5 3B Q4_K_M ;
- 80 à 120 tokens/s sur Qwen 2.5 0.5B Q8 (le Triage) ;
- 2 à 5 secondes pour la phase de prefill (lecture du prompt) ;
- 8 à 15 secondes pour la génération typique d’une décision SOC.
Avec les transitions LangGraph et la sérialisation, on aboutit à une latence end-to-end de 15 à 30 secondes par incident, ce qui correspond exactement à la cible posée à la conception.
Un bénéfice inattendu : la quantification Q4_K_M, présentée souvent comme un compromis qualité, n’a pas dégradé sensiblement la qualité des décisions sur ce cas d’usage. Plusieurs hypothèses expliquent ce constat — le contexte est borné par le LoRA, la sortie est forcée par schéma, le raisonnement nécessaire est local plutôt que abstrait. La leçon généralisable : dans un système typé qui contraint fortement entrée et sortie, on peut quantifier agressivement sans sacrifier la fiabilité.
Cascade vs agent unique : le tradeoff observé
L’article 2 défendait la cascade Triage → SOC → CERT par un argument théorique. Voici la mesure :
Sur un volume représentatif d’alertes Wazuh (rule levels 5 à 13), avec la cascade activée :
- ~70 % des alertes s’arrêtent à Triage (
dismiss) — 2 à 4 secondes de pipeline total ; - ~25 % vont jusqu’au SOC sans CERT — 15 à 20 secondes ;
- ~5 % vont au CERT — 25 à 35 secondes.
Latence moyenne pondérée : ~7 secondes par alerte.
Sans la cascade, en faisant tourner directement le SOC sur tout : 15 à 20 secondes par alerte, soit une latence multipliée par ~2.5 et un coût CPU global beaucoup plus élevé. Pour un service qui doit absorber un pic de quelques milliers d’alertes/jour, le facteur 2.5 fait la différence entre “ça tient” et “ça déborde”.
Le tradeoff inverse — la cascade introduit deux frontières supplémentaires donc deux occasions de ValidationError — est mesuré mais reste marginal grâce aux boucles de correction locales (article 1).
Faux positifs vs faux négatifs : sur quoi on agit
Question récurrente quand on parle d’IA dans la sécurité : que faire quand le système se trompe ?
La discipline qu’on retient : les deux types d’erreur ne se corrigent pas au même endroit.
- Faux positifs (alertes anodines bloquées) → ajustement du prompt système et du contexte fourni à l’agent. La règle métier qu’on veut affiner est rarement dans les poids du LoRA ; elle est dans la façon dont on lui présente le problème. Itérer sur le prompt est rapide et auditable.
- Faux négatifs (vraies attaques laissées passer) → là c’est potentiellement plus grave, et la correction passe par l’enrichissement du jeu d’entraînement du LoRA correspondant (cas non couvert dans la version actuelle, à intégrer comme exemple positif typé) puis ré-entraînement de l’adaptateur.
La séparation est importante. Tout corriger par re-fine-tuning est un anti-pattern : on perd l’auditabilité, on injecte du bruit dans les poids et on rend le système moins prévisible. À l’inverse, tout corriger par prompt est limité — il y a des cas où le modèle de base manque réellement de connaissance, et seul un re-entraînement ciblé règle le problème.
La règle empirique : commencer par le prompt, finir par le LoRA.
La leçon-clé
Si on devait retenir une seule chose de l’expérience asp-forge, ce serait celle-ci :
Un SOC agentique n’est pas un agent autonome. C’est un système de décision typé où le LLM remplace l’analyste sur le triage L1, pas le décideur final.
Tous les choix structurants — graphe d’états plutôt que conversation, schémas Pydantic aux frontières, whitelists d’actions, réversibilité des effets, humain dans la boucle pour les décisions à fort impact — découlent de cette ligne. C’est ce qui distingue une démo viralité LinkedIn d’un système qu’on accepterait de mettre devant un firewall en production.
Le LLM est un outil de raisonnement bon marché et flexible. Il n’est pas un opérateur. La discipline d’ingénierie consiste à exploiter ses qualités (compréhension contextuelle, formulation en langage naturel) sans s’exposer à ses défauts (non-déterminisme, sensibilité aux injections, hallucinations). Un système qui réussit cette discipline est utile en production. Un système qui l’ignore finit en démo qui marche en photo et se casse à la première mise en charge.
Et après asp-forge ?
asp-forge est passé en archivage après sa dernière démo. Pas par échec — au contraire, beaucoup des leçons capturées ont alimenté un projet successeur dédié au pentest agentique (orchestrateur purpleteam-orchestrator, qui réutilise les patterns clés : graphes d’états, validation typée aux frontières, whitelist par construction, humain dans la boucle).
Ce qui se reprend tel quel :
- la discipline des frontières typées ;
- les graphes d’états explicites plutôt que conversationnel ;
- la réversibilité par UUID des actions sur les systèmes externes ;
- la cascade de modèles par capacités croissantes pour amortir les volumes ;
- l’auto-block immédiat + revue par LLM sur les catégories à haut niveau de criticité.
Ce qui change dans la nouvelle itération :
- le pipeline LangGraph est remplacé par Burr (graphes d’états similaires mais plus léger côté Python) couplé à pydantic-ai pour la couche LLM ;
- l’angle métier glisse du triage SOC vers le pentest automatisé (recon réseau, K8s posture assessment) ;
- la cible n’est plus un firewall mais un rapport cyber typé — posture A-F par domaine, gaps actionnables, kill chains corrélées.
C’est un autre projet, sur lequel reviendront sans doute des articles ultérieurs. Mais asp-forge reste la première itération qui a montré que le SOC agentique self-hosted n’est pas un fantasme académique — c’est un système ingénierable, opérable et auditable quand on accepte d’imposer la discipline typée que cette série a tenté de documenter.
Fin de la série.
Article 1 — Pourquoi un SOC agentique self-hosted ? Architecture et choix techniques
Article 2 — Le pipeline agentique : cascade Triage → SOC → CERT
