Victor : anonymiseur de logs de sécurité souverain et auto-apprenant
Avant d’envoyer des logs à un éditeur, de les injecter dans un LLM externe ou de les archiver conformément au RGPD, une question se pose inévitablement : ces fichiers contiennent-ils des informations qui exposent mon infrastructure ?
Les logs de sécurité sont denses en données sensibles — adresses IP internes, noms d’hôtes, identifiants de comptes de service, clés API, adresses MAC. Et contrairement aux bases de données ou aux formulaires, leur format n’est jamais tout à fait uniforme : chaque équipement, chaque version de daemon, chaque intégration produit ses propres variantes. Un Fortigate ne logue pas comme CrowdSec. Un OPNsense ne logue pas comme un agent Wireguard.
Le réflexe naturel est d’écrire des expressions régulières. Et ça fonctionne — jusqu’à un certain
point. Les IPs RFC1918 ? Une regex couvre ça facilement. Mais fw01-dmz, svc_backup_agent,
crowdsec-bouncer-api ? Ces entités n’ont pas de forme canonique. Leur structure dépend des
conventions de nommage de l’organisation, et elles changent au fil des déploiements. Maintenir un
catalogue de regex pour tout couvrir devient vite un travail à part entière — et on n’en voit
jamais vraiment le bout.
Il manquait un outil capable de comprendre le contexte, de détecter des entités qu’on n’avait pas anticipées, et de s’améliorer au fil des logs traités. C’est pour ça que Victor existe.
Ce que fait Victor
Victor) détecte et remplace les entités sensibles par des tokens pseudonymisés cohérents sur toute une session :
Block IP 192.168.1.50 on fw01.company.local — CVE-2024-12345
→ Block IP {{IP_PRIVE}} on {{HOSTNAME}} — {{CVE_ID}}
La même entité reçoit toujours le même token. Si fw01.company.local apparaît dans dix fichiers
de logs différents d’un même batch, il sera {{HOSTNAME}} partout — ce qui préserve la
corrélation sans exposer l’information.
Architecture : deux couches complémentaires
Victor combine deux mécanismes de détection :
texte brut
└─► CustomRulesProcessor (regex : IPs RFC1918, CVE, MAC, hostnames…)
└─► NERProcessor (spaCy : modèle AnonyNER — entités cyber)
└─► ReplacementGenerator (tokens cohérents : HOST_001, IP_001…)
└─► texte anonymisé + mapping + rapport
Les règles regex couvrent les catégories les plus fréquentes dans les logs cyber :
IPs RFC1918, identifiants CVE, FQDNs de zones privées (.local, .corp, .lan),
hostnames numérotés (fw01, srv-003), adresses MAC, tokens hex 32–64 caractères.
AnonyNER est un modèle spaCy entraîné spécifiquement sur des entités cybersécurité.
Là où un modèle NLP généraliste voit une suite de mots ordinaires, AnonyNER reconnaît
crowdsec-bouncer-http, wireguard_peer_pat, ou svc_backup_agent pour ce qu’ils sont :
des identifiants internes qui n’ont rien à faire dans un fichier partagé.
Les deux couches se complètent : les règles regex gèrent les patterns prévisibles avec certitude, AnonyNER prend en charge tout ce qui relève du contexte et de la sémantique.
Traitement par batch de fichiers
Victor traite des répertoires entiers de logs en une seule passe. Tous les fichiers d’un
batch partagent la même session d’anonymisation — la cohérence des tokens est garantie
entre firewall.log, ids.log et crowdsec.log.
from pathlib import Path
from victor import LogProcessor
processor = LogProcessor(
inbox_dir = Path("logs/inbox"),
outbox_dir = Path("logs/outbox"),
)
report = processor.process_batch()
Les fichiers traités sont routés automatiquement :
| Répertoire | Condition |
|---|---|
clean/ |
0 gap résiduel — anonymisation complète |
partial/ |
Gaps résiduels — revue recommandée |
error/ |
Échec de traitement |
Un batch_mapping.json enregistre le mapping global {valeur_originale: token} pour
l’ensemble du batch — utile pour la traçabilité ou la désanonymisation ultérieure.
La boucle d’auto-apprentissage
C’est la partie la plus intéressante. Victor ne se contente pas d’anonymiser — il observe ses propres lacunes.
Un gap NER est une entité détectée par AnonyNER mais non prise en charge par le moteur d’anonymisation. C’est un signal : le modèle voit quelque chose que les règles ne couvrent pas.
anonymize_*()
└─► ner_gaps (entités non anonymisées)
└─► GapCollector.record()
└─► fréquence N / M sessions
└─► candidates()
├─► to_regex_rule() → RuleWriter → custom_rules.json
└─► to_spacy_examples() → AnnotationWriter → train.spacy
Le GapCollector agrège ces entités manquantes à travers les sessions, les score par fréquence
et propose deux types d’actions :
- Piste courte : générer une règle regex et l’ajouter à
custom_rules.json— effet immédiat - Piste longue : générer un exemple d’entraînement spaCy et l’ajouter au dataset — amélioration du modèle
La validation humaine est l’unique verrou avant toute écriture. Le GapCollector ne modifie
rien de lui-même.
Validation automatique par SLM local
Pour les environnements qui génèrent beaucoup de gaps, Victor peut déléguer la validation
à un SLM local via Ollama. Aucun GPU requis — qwen2.5:1.5b (~1 Go) tourne sur CPU.
from victor import GapValidator
validator = GapValidator() # Ollama localhost:11434, qwen2.5:1.5b
results = validator.validate_candidates(collector)
# ACCEPT → règle ou exemple d'entraînement
# REJECT → blacklisté
# unsure → reste en attente de validation humaine
Si Ollama n’est pas disponible, le validateur retourne decision="unsure" sans exception —
la validation humaine reste toujours possible indépendamment.
Le choix de qwen2.5:1.5b est délibéré : la tâche est une classification binaire (cet élément
est-il une entité sensible de type X ?), pas un raisonnement complexe. Un modèle léger sur CPU
est suffisant et évite toute dépendance GPU.
Un projet souverain
Victor est conçu pour fonctionner sans aucune dépendance externe au-delà de spaCy. Pas d’appel API, pas de modèle cloud, pas de télémétrie. Les logs restent sur votre infrastructure.
Le projet est publié sous AGPL-3.0 — toute version modifiée, y compris utilisée comme service réseau, doit redistribuer son code source sous la même licence. Une licence commerciale dérogatoire est disponible pour les organisations dont les politiques internes sont incompatibles avec cette obligation (à l’exception du secteur de la défense et de l’armement).
Le code source est disponible sur GitHub.
Résultats sur logs réels
Premier test sur un jeu de logs public issu du projet LogHub :
un fichier Linux syslog de 211 Ko (Linux_2k.log, 2 000 lignes).
| Fichier | Taille | Remplacements | Tokens générés | Statut | Gaps résiduels |
|---|---|---|---|---|---|
Linux_2k.log |
211 Ko | 3 287 | 1 676 | partial | 1 |
Le moteur a effectué 3 287 remplacements sur un seul fichier, produisant 1 676 tokens
distincts. Un seul gap résiduel : AnonyNER classifie selinux_register_security: comme
MAC_ADDRESS — faux positif sans impact réel.
Ce que révèle le mapping
L’examen du batch_mapping.json illustre les limites actuelles du modèle sur ce format :
| Entrée capturée | Token | Verdict |
|---|---|---|
sshd(pam_unix)[19937]: |
{{IFACE_001}} |
❌ Process+PID → Interface |
14:53:32 |
{{IP_001}} |
❌ Timestamp → IP |
12:13:20 |
{{FW_RULE_002}} |
❌ Timestamp → Firewall Rule |
]: |
{{MAC_001}} |
❌ Ponctuation → MAC |
uid=0 |
{{PORT_001}} |
❌ UID root → Port |
rhost=220-135-151-1.hinet-ip.hinet.net |
{{HOST_001}} |
⚠️ Correct, préfixe rhost= inclus |
AnonyNER v3 a été entraîné sur des logs réseau et firewall. Les Linux syslogs ont un format
structuré différent — process(subsystem)[pid]: — que le modèle ne reconnaît pas encore.
Ce résultat est documenté, pas masqué. Il justifie précisément l’existence de la boucle d’auto-apprentissage et du pipeline d’entraînement.
Entraîner ou ré-entraîner AnonyNER
Victor embarque un répertoire training/ avec les scripts et données pour ré-entraîner
ou enrichir le modèle AnonyNER directement à partir des gaps de production.
# 1. Compiler le corpus JSONL → format binaire spaCy
python training/scripts/prepare_spacy_dataset.py \
--input training/data/anonyner_train.jsonl \
--train-out training/data/train.spacy \
--dev-out training/data/dev.spacy
# 2. Entraîner (CPU, pas de GPU requis)
python training/scripts/train_anonyner.py
# → training/models/anonyner_model/model-best
La boucle naturelle : gaps de production → exemples annotés → ré-entraînement.
from victor import GapCollector, AnnotationWriter
from pathlib import Path
collector = GapCollector(data_dir=Path("data"))
ann_writer = AnnotationWriter(data_dir=Path("data/dataset"))
for gap in collector.candidates(min_occurrences=3):
examples = collector.to_spacy_examples(gap["text"], gap["label"])
ann_writer.add_examples(examples, label=gap["label"],
source_key=f"gap::{gap['label']}")
collector.accept(gap["text"], gap["label"])
# → data/dataset/annotations.json → training/data/ → ré-entraînement
Chaque batch de logs traités est une opportunité d’améliorer le modèle pour les suivants.
Limitations connues
Tokens statiques pour les custom rules — plusieurs IPs distinctes capturées par la même
règle regex reçoivent le même token ({{IP_PRIVE}}). La numérotation séquentielle
({{IP_001}}, {{IP_002}}) ne s’applique qu’aux entités détectées par AnonyNER.
Modèle de fallback — sans AnonyNER, le modèle générique en_core_web_md ne détecte pas
les entités cyber. Les règles regex fonctionnent dans les deux cas.
Spécialisation firewall/réseau — AnonyNER v3 a été entraîné principalement sur des logs OPNsense, CrowdSec et WireGuard. Les logs applicatifs Linux (syslog, journald) ou Windows (Event Log) nécessitent un enrichissement du corpus avant d’obtenir des résultats fiables.
Victor est un outil de terrain. Il part du constat que les logs de production sont hétérogènes, que les conventions de nommage varient d’une infra à l’autre, et qu’aucun catalogue de regex ne sera jamais exhaustif. La boucle gaps → règles → dataset → entraînement est là pour ça : chaque corpus traité affine la détection, et ce qui était un angle mort aujourd’hui devient une règle ou un exemple d’entraînement demain.
by Patrice Le Guyader
