TP 1 — Détection as Code : Sigma → LogQL
Auteur : Thibaut Fontaine Institution : Université de la Réunion Date : 2026
Table des matières
Section intitulée « Table des matières »- Objectifs et Contexte
- Prérequis
- Étape 0 — Reconnaissance des sources Loki (R0)
- Étape 1 — Écrire sa première règle Sigma (R1)
- Étape 2 — Compiler Sigma → LogQL (R2)
- Étape 3 — Détecter l’attaque jour J
- Étape 4 — Versionner les règles (CI détection-as-code)
- Pièges & bonnes pratiques
- Livrables & validation
- Annexe enseignant — corrigé
1. Objectif & contexte
Section intitulée « 1. Objectif & contexte »📡 Poste SOC · La Réunion — Acte 2, Détecter : vous armez Alfred Faure (Crozet), ~2 900 km. Liaison SATCOM nominale.
La supervision voit (Acte 1). Ici on lui apprend à reconnaître une menace — sans changer de stack : mêmes logs, même Loki, même Grafana.
Le SIEM n’est pas un produit séparé : c’est de la détection-as-code posée sur la stack d’observabilité de l’Acte 1. L’asset central est la règle Sigma — un standard portable, mappé MITRE ATT&CK, que vous écrivez une fois et qui se compile vers n’importe quel backend (Loki, Splunk, Elastic, Sentinel…).
🔴 Au fil de l’enquête
Section intitulée « 🔴 Au fil de l’enquête »Le jour J, l’attaque a laissé des traces dans vos logs : phishing e.bernard (08:00Z), authentification VPN depuis 203.0.113.42 (09:12Z), ouverture du coffre CLASSIFIED-VAULT-X (10:05Z), exfil de genetic_samples (10:12Z). Tant que personne n’écrit la règle qui les reconnaît, elles défilent sans alerte. Ici vous écrivez cette règle — la première qui aurait fait sonner le jour J.
Objectifs pédagogiques
Section intitulée « Objectifs pédagogiques »- Comprendre l’asymétrie SIEM : le coût est dans la règle, pas dans le moteur.
- Écrire une règle Sigma valide (logsource, detection, condition, tags ATT&CK).
- Compiler vers LogQL avec
pySigma-backend-lokiet lire la requête générée. - Détecter 3 étapes concrètes de la kill chain du jour J dans Grafana Explore.
- Poser le repo Git qui servira de base à la CI détection-as-code (TP2/TP3).
Architecture du TP1
Section intitulée « Architecture du TP1 »graph LR subgraph "Sources (Acte 1)" FALCO[Falco
runtime] AUTH[Auth VPN/AD
auth.log] PG[PostgreSQL
access_log] end subgraph "Stockage" LOKI[(Loki
:3100)] end subgraph "Détection-as-Code" RULES[/Repo Git
règles Sigma .yml/] SIGMA[sigma CLI
+ pySigma-backend-loki] LOGQL["LogQL généré"] end subgraph "Investigation" EXPLORE[Grafana Explore
:3000] end FALCO --> LOKI AUTH --> LOKI PG --> LOKI RULES --> SIGMA SIGMA --> LOGQL LOGQL --> EXPLORE LOKI --> EXPLORE classDef src fill:#ffcc99,stroke:#333,stroke-width:2px classDef store fill:#99ccff,stroke:#333,stroke-width:2px classDef code fill:#99ff99,stroke:#333,stroke-width:2px classDef ui fill:#ffccff,stroke:#333,stroke-width:2px class FALCO,AUTH,PG src class LOKI store class RULES,SIGMA,LOGQL code class EXPLORE ui
2. Prérequis
Section intitulée « 2. Prérequis »Sur la VM soc
Section intitulée « Sur la VM soc »vagrant ssh soc
# Vérifier que Python 3 est dispopython3 --version # Python 3.10+ attendu
# Installer Sigma CLI + le backend Lokipython3 -m venv ~/sigma-venvsource ~/sigma-venv/bin/activatepip install --upgrade pippip install sigma-cli==1.0.* pysigma-backend-loki==0.13.* pysigma-pipeline-loki==0.13.*
sigma version # → sigma-cli 1.0.xsigma plugin list # → loki backend, loki pipeline (installés)Vérifier Loki et Grafana
Section intitulée « Vérifier Loki et Grafana »Depuis la VM soc (joint la VM monitoring par IP) :
curl -s http://IP_VM2:3100/ready # → readycurl -s http://IP_VM2:3000/api/health # → {"database":"ok",...}Ouvrez Grafana dans votre navigateur :
http://IP_VM2:3000 (admin / AdminTAAF2024!)Dans Explore (icône boussole), datasource Loki, lancez :
{job=~".+"} | json | __error__=""→ Vous devez voir défiler des logs. Si rien ne s’affiche, vérifiez que la VM monitoring tourne et que les logs de l’incident sont peuplés (Acte 0 — Phase 4).
3. Étape 0 — Reconnaissance des sources Loki (R0)
Section intitulée « 3. Étape 0 — Reconnaissance des sources Loki (R0) »Avant d’écrire une règle, il faut savoir quel champ contient quoi dans Loki. C’est la phase « 90 % de la détection ».
3.1 — Lister les labels et les jobs disponibles
Section intitulée « 3.1 — Lister les labels et les jobs disponibles »Dans Grafana Explore (datasource Loki), cliquez sur Label filters > job :
job attendu | Source |
|---|---|
falco | Événements Falco (runtime sensitive file, exec, network) |
auth | Logs auth VPN + AD (02_auth_vpn_ad.log) |
postgres | Logs PostgreSQL base_af |
windows | Windows Event Logs (Sysmon + Security) |
zeek_ssl / zeek_conn / zeek_arp | Zeek (réseau, downgrade PQC, MitM) |
sat_telemetry | Télémétrie SATCOM |
3.2 — Reconnaître la structure d’un log Falco
Section intitulée « 3.2 — Reconnaître la structure d’un log Falco »{job="falco"} | json | priority="Critical"Cliquez sur une ligne pour voir les champs JSON : time, rule, priority, output_fields.user.name, output_fields.fd.name…
3.3 — Reconnaître la structure d’un log d’auth
Section intitulée « 3.3 — Reconnaître la structure d’un log d’auth »{job="auth"} | logfmt | user="e.bernard"Champs attendus : user, src_ip, auth_status, service (vpn/ad).
3.4 — Comptage rapide pour repérer un pic
Section intitulée « 3.4 — Comptage rapide pour repérer un pic »sum by (job) (count_over_time({job=~".+"}[5m]))→ Repérez quels jobs sont actifs autour des heures clés (08:00Z, 09:12Z, 10:05Z, 10:12Z).
4. Étape 1 — Écrire sa première règle Sigma (R1)
Section intitulée « 4. Étape 1 — Écrire sa première règle Sigma (R1) »On va écrire 3 règles couvrant 3 étapes de la kill chain du jour J. Vous commencez par la plus simple, puis les deux autres en autonomie.
4.1 — Initialiser le repo
Section intitulée « 4.1 — Initialiser le repo »Sur la VM soc :
mkdir -p ~/taaf-detections/rulescd ~/taaf-detectionsgit initmkdir -p rules/{initial_access,credential_access,collection,exfiltration}echo "*.pyc" > .gitignore4.2 — Règle #1 : VPN depuis une IP externe non listée
Section intitulée « 4.2 — Règle #1 : VPN depuis une IP externe non listée »Intention de détection : l’utilisateur e.bernard s’authentifie sur le VPN depuis une IP qui n’est ni sur le réseau interne TAAF (10.42.0.0/16) ni sur les IP « connues » (VPN sortants légitimes). C’est l’étape 09:12Z du jour J (203.0.113.42).
Créez rules/initial_access/vpn-auth-external-ip.yml :
title: VPN Authentication from External IP (TAAF)id: 8c1a7b4e-1c2d-4f3a-9e1b-5a6f7c8d9e10status: experimentaldescription: | Détecte une authentification VPN réussie pour un utilisateur TAAF depuis une IP source qui n'est pas dans le pool interne (10.42.0.0/16) ni dans les IP de sortie connues (sites partenaires).references: - https://attack.mitre.org/techniques/T1078/ - https://attack.mitre.org/techniques/T1133/author: TAAF SOCdate: 2026/06/16logsource: product: vpn service: authdetection: selection: service: 'vpn' auth_status: 'success' filter_internal: src_ip|startswith: - '10.42.' - '192.168.' filter_known: src_ip: - '198.51.100.10' # IP partenaire INRIA - '198.51.100.11' # IP partenaire CNRS condition: selection and not (filter_internal or filter_known)fields: - user - src_ip - servicefalsepositives: - Utilisateur en déplacement déclaré (vérifier dans le calendrier RH) - Nouvelle IP partenaire pas encore ajoutée à filter_knownlevel: hightags: - attack.initial_access - attack.t1078 - attack.t1133Lecture du fichier :
logsource: ce que la règle vise — produit + service (utilisé par les pipelines pour mapper les champs).detection.selection: ce qu’on attrape (auth VPN réussie).detection.filter_*: ce qu’on exclut (IPs légitimes).condition: combinaison logique (and not) → seule l’IP suspecte reste.tags: techniques ATT&CK pour le TP3 (couverture).
4.3 — Règle #2 : Lecture non autorisée du coffre CLASSIFIED-VAULT-X
Section intitulée « 4.3 — Règle #2 : Lecture non autorisée du coffre CLASSIFIED-VAULT-X »Intention : Falco lève une alerte quand un fichier de CLASSIFIED-VAULT-X est lu par un utilisateur autre que la liste d’administrateurs habilités. C’est l’étape 10:05Z (mouvement latéral vers le serveur de fichiers).
D’abord, écrivez la règle Falco côté runtime (déposez-la dans le repo monitoring, fichier falco/rules.d/taaf-classified-vault.yaml) :
- list: vault_authorized_users items: [admin_cloud_af, root]
- rule: Unauthorized Read of CLASSIFIED Vault desc: > Lecture d'un fichier dans le coffre CLASSIFIED-VAULT-X par un compte non habilité (hors admin NextCloud légitime). condition: > open_read and fd.name startswith "/var/www/html/data/admin.cloud.af/files/CLASSIFIED-VAULT-X" and not user.name in (vault_authorized_users) output: > Unauthorized read of CLASSIFIED vault (user=%user.name proc.name=%proc.name proc.cmdline=%proc.cmdline file=%fd.name) priority: CRITICAL tags: [taaf, classified, t1213]Rechargez Falco :
sudo systemctl reload falco-modern-bpfjournalctl -u falco-modern-bpf -n 20 # vérifier que la règle est bien chargéePuis la règle Sigma côté SIEM (rules/collection/falco-classified-vault-read.yml) :
title: Falco — Unauthorized Read of CLASSIFIED Vaultid: a4f7c2d1-9e8b-4a3f-b6c5-1d2e3f4a5b6cstatus: stabledescription: Une alerte Falco Critical sur l'ouverture du coffre CLASSIFIED-VAULT-X par un utilisateur non listé.references: - https://attack.mitre.org/techniques/T1213/author: TAAF SOCdate: 2026/06/16logsource: product: falcodetection: selection: rule: 'Unauthorized Read of CLASSIFIED Vault' priority: 'Critical' condition: selectionfields: - output_fields.user.name - output_fields.proc.name - output_fields.fd.namelevel: criticaltags: - attack.collection - attack.t12134.4 — Règle #3 (autonomie) : Exfil genetic_samples sans approved_by
Section intitulée « 4.4 — Règle #3 (autonomie) : Exfil genetic_samples sans approved_by »Intention : à 10:12Z, l’attaquant exécute pg_dump sur genetic_samples (KER-2024-002) sans renseigner approved_by. Le SI métier impose qu’aucune requête classifiée ne passe sans approbation tracée.
À VOUS de l’écrire — fichier
rules/exfiltration/postgres-genetic-samples-no-approval.yml.Indices :
logsource.product: postgresql,service: access_log- selection sur
query|contains: 'genetic_samples'etapproved_by: NULL- tag :
attack.exfiltration+attack.t1041
(Corrigé en Annexe.)
5. Étape 2 — Compiler Sigma → LogQL (R2)
Section intitulée « 5. Étape 2 — Compiler Sigma → LogQL (R2) »5.1 — Conversion en ligne de commande
Section intitulée « 5.1 — Conversion en ligne de commande »cd ~/taaf-detectionssource ~/sigma-venv/bin/activate
sigma convert -t loki -p loki_grafana_logfmt \ rules/initial_access/vpn-auth-external-ip.ymlSortie attendue (approx) :
{job="auth"} | logfmt | service=`vpn` | auth_status=`success` | src_ip!~`^(10\.42\.|192\.168\.).*` | src_ip!~`^(198\.51\.100\.10|198\.51\.100\.11)$`5.2 — Conversion en batch (tout le repo)
Section intitulée « 5.2 — Conversion en batch (tout le repo) »Créez scripts/compile.sh :
#!/usr/bin/env bashset -euo pipefailOUT="build/logql"mkdir -p "$OUT"find rules -name "*.yml" -type f | while read -r rule; do name=$(basename "$rule" .yml) echo "[+] $rule → $OUT/$name.logql" sigma convert -t loki -p loki_grafana_logfmt "$rule" > "$OUT/$name.logql"doneecho "[✓] $(ls "$OUT" | wc -l) règles compilées"chmod +x scripts/compile.sh./scripts/compile.shSortie attendue :
[+] rules/initial_access/vpn-auth-external-ip.yml → build/logql/vpn-auth-external-ip.logql[+] rules/collection/falco-classified-vault-read.yml → build/logql/falco-classified-vault-read.logql[+] rules/exfiltration/postgres-genetic-samples-no-approval.yml → build/logql/postgres-genetic-samples-no-approval.logql[✓] 3 règles compilées5.3 — Lire la requête générée
Section intitulée « 5.3 — Lire la requête générée »Lecture obligatoire avant de l’exécuter :
- Label filters (
{job="auth"}) : à gauche du|. Définissent quels streams Loki sont scannés. Choisir ici réduit la cardinalité. - Line filters (
|= "foo") : matching texte brut, très rapide. - Parsers (
| logfmt,| json) : extraient les champs depuis la ligne. - Label filters post-parsing (
| user="e.bernard") : filtrage sur champs extraits.
Règle d’or de cardinalité (Loki) : job, host, severity en labels — IP/user/path dans la ligne (extraits par parseur). Sinon Loki explose.
6. Étape 3 — Détecter l’attaque jour J
Section intitulée « 6. Étape 3 — Détecter l’attaque jour J »6.1 — Exécuter la règle #1 dans Explore
Section intitulée « 6.1 — Exécuter la règle #1 dans Explore »Ouvrez Grafana → Explore → Datasource Loki. Collez la requête générée à l’étape 5.1.
Résultat attendu : une ligne (au moins) autour de 09h12 UTC du jour J — voici le log brut que Loki doit remonter (généré par data/taaf_log_generator, job auth) :
ts=2026-06-16T09:12:43Z service=vpn user=e.bernard src_ip=203.0.113.42 src_country=NL auth_status=success vpn_node=vpn-paf-01 session_id=a7f3c9e1Pourquoi cette ligne tombe sur la règle :
service=vpn✓ matcheselection.serviceauth_status=success✓ matcheselection.auth_statussrc_ip=203.0.113.42ne commence ni par10.42.ni par192.168.(pas dansfilter_internal) et n’est pas dansfilter_known→ l’IP passe lenot (filter_*)→ l’événement est gardé
📸 Capture obligatoire : screenshot de Grafana Explore montrant la détection. Annotez-y le timestamp et l’IP suspecte.
6.2 — Exécuter la règle #2 (Falco)
Section intitulée « 6.2 — Exécuter la règle #2 (Falco) »{job="falco"} | json | rule=`Unauthorized Read of CLASSIFIED Vault`Résultat attendu : un événement Critical autour de 10h05 UTC sur KER-2024-002.fasta. Le log Falco que vous devez voir :
{ "time": "2026-06-16T10:05:21.435Z", "rule": "Unauthorized Read of CLASSIFIED Vault", "priority": "Critical", "source": "syscall", "output": "Unauthorized read of CLASSIFIED vault (user=www-data proc.name=php-fpm proc.cmdline=php-fpm: pool www file=/var/www/html/data/admin.cloud.af/files/CLASSIFIED-VAULT-X/KER-2024-002.fasta)", "output_fields": { "user.name": "www-data", "proc.name": "php-fpm", "proc.cmdline": "php-fpm: pool www", "fd.name": "/var/www/html/data/admin.cloud.af/files/CLASSIFIED-VAULT-X/KER-2024-002.fasta", "evt.time": 1750068321435000000 }, "tags": ["taaf", "classified", "t1213"]}Lecture : user.name=www-data n’est pas dans la liste vault_authorized_users (qui contient admin_cloud_af, root) → la règle Falco lève Critical → Sigma → LogQL la remonte.
📸 Capture obligatoire : screenshot Grafana Explore avec le JSON Falco déplié (clic sur la ligne).
6.3 — Exécuter la règle #3 (votre rédaction)
Section intitulée « 6.3 — Exécuter la règle #3 (votre rédaction) »À vous d’écrire la LogQL résultante et de la valider sur le job postgres. Cible : la requête pg_dump genetic_samples à 10h12 UTC.
Résultat attendu : une ligne du job postgres correspondant à l’extraction de la table classifiée sans approbation :
ts=2026-06-16T10:12:08Z db=base_af user=svc_backup client_ip=10.42.30.12 application=pg_dump duration_ms=247 rows=1 query="COPY (SELECT * FROM genetic_samples WHERE sample_id='KER-2024-002') TO STDOUT" approved_by=nullPourquoi cette ligne tombe sur la règle :
querycontientgenetic_samples✓approved_byestnull✓ (workflow d’approbation contourné)- L’application est
pg_dump(extraction massive, signal très fort dans un contexte d’admin)
Indice supplémentaire : la même fenêtre temporelle contient aussi des SELECT légitimes par l’application métier — votre règle ne doit pas matcher ceux-ci (ils ont approved_by non-null).
📸 Capture obligatoire : screenshot Grafana Explore montrant la requête malveillante isolée.
6.4 — Bonus : voir les 3 alertes sur un même graphique temporel
Section intitulée « 6.4 — Bonus : voir les 3 alertes sur un même graphique temporel »Dans Explore, basculez en mode graph et utilisez :
sum by (rule) ( count_over_time({job=~"falco|auth|postgres"} | logfmt [5m]))→ Vous voyez la chronologie de l’attaque se dessiner. C’est le socle du TP3 (corrélation).
7. Étape 4 — Versionner les règles
Section intitulée « 7. Étape 4 — Versionner les règles »cd ~/taaf-detectionsgit add rules/ scripts/ .gitignoregit commit -m "feat(detections): TP1 — 3 règles Sigma jour J (T1078/T1213/T1041)"
# Pousser sur votre repo perso (GitLab)git remote add origin git@gitlab.com:VOTRE-LOGIN/taaf-detections.gitgit push -u origin main8. Pièges & bonnes pratiques
Section intitulée « 8. Pièges & bonnes pratiques »| Piège | Symptôme | Solution |
|---|---|---|
| Logsource Sigma mal mappé | sigma convert ne sort rien (silent fail) | Lire le mapping loki_grafana_logfmt, ajuster product/service |
| Filtres Loki sur IP en label | Cardinalité explose, Loki devient lent | Mettre l’IP dans la ligne, parsée via ` |
| Backend Loki mal pinné | LogQL généré obsolète (syntaxe Loki 2.x vs 3.x) | Pinner pysigma-backend-loki==0.13.* |
condition sans not autour des filtres | Faux positifs ou faux négatifs | Toujours selection and not (filter_X or filter_Y) |
Pas de tags: attack.txxxx | Pas de couverture ATT&CK au TP3 | Tagger systématiquement (norme MITRE) |
Règle sans falsepositives: | Tunage impossible au TP2 | Lister les FP connus dès l’écriture |
9. Livrables & validation
Section intitulée « 9. Livrables & validation »Livrables (à pousser sur GitLab)
Section intitulée « Livrables (à pousser sur GitLab) »- Repo
taaf-detectionscontenant :rules/initial_access/vpn-auth-external-ip.ymlrules/collection/falco-classified-vault-read.ymlrules/exfiltration/postgres-genetic-samples-no-approval.ymlscripts/compile.shexécutableREADME.mdlistant les 3 règles avec un lien ATT&CK
- Règle Falco custom déposée dans le repo
monitoring(falco/rules.d/taaf-classified-vault.yaml). - 3 captures Grafana Explore (PNG) montrant chacune une détection avec timestamp visible.
Grille de validation
Section intitulée « Grille de validation »| Critère | Pondération |
|---|---|
Les 3 règles Sigma compilent sans erreur (sigma convert exit 0) | 25 % |
| LogQL généré correct (testé dans Explore, retourne ≥ 1 ligne sur les logs du jour J) | 25 % |
tags: attack.txxxx présent et cohérent dans chacune des 3 règles | 15 % |
Falco rule custom déployée et visible dans journalctl -u falco-modern-bpf | 10 % |
Repo Git propre (commits atomiques, README, .gitignore) | 10 % |
| Captures Grafana exploitables (lisibles, timestamps annotés) | 15 % |
Format de rendu : PDF nom-prenom-promo.pdf sur Moodle (cf. règle Acte 1). -5 points si nommage non respecté.
10. Annexe enseignant — corrigé
Section intitulée « 10. Annexe enseignant — corrigé »Corrigé de la règle #3 (exfil genetic_samples)
Section intitulée « Corrigé de la règle #3 (exfil genetic_samples) »rules/exfiltration/postgres-genetic-samples-no-approval.yml :
title: PostgreSQL — Genetic Samples Access without Approvalid: f3a4b5c6-d7e8-4f9a-b0c1-d2e3f4a5b6c7status: experimentaldescription: | Détecte une requête SQL sur la table classifiée `genetic_samples` sans renseignement du champ `approved_by` (workflow d'approbation obligatoire pour les données CLASSIFIED).references: - https://attack.mitre.org/techniques/T1041/ - https://attack.mitre.org/techniques/T1213/author: TAAF SOCdate: 2026/06/16logsource: product: postgresql service: access_logdetection: selection: query|contains: 'genetic_samples' approved_by: null condition: selectionfields: - user - query - approved_by - dbfalsepositives: - Requête d'administration (DDL/DCL) sur la table — ne devrait jamais arriver en prodlevel: criticaltags: - attack.collection - attack.t1213 - attack.exfiltration - attack.t1041LogQL générée :
{job="postgres"} | json | query=~`.*genetic_samples.*` | approved_by=``Évaluation des erreurs courantes
Section intitulée « Évaluation des erreurs courantes »condition: selection and selection_2au lieu decondition: selection and not filter→ l’étudiant n’a pas compris la logique d’exclusion. -10 %.- Règle sans
id→ invalide pour le repo de détection-as-code. -5 %. - Tags ATT&CK absents ou
attack.txxxxsans le préfixeattack.→ S2AN au TP3 ne pourra pas générer le layer. -10 %. - Hardcode d’IP dans la règle sans
filter_known→ règle non maintenable. -5 %.
Pont vers le TP2
Section intitulée « Pont vers le TP2 »À la fin du TP1, l’étudiant a 3 règles qui se matchent dans Explore. Au TP2, la même règle Sigma se compile avec un autre backend Sigma (grafana_alerting) pour produire une alerte Grafana routée vers Discord. La règle ne change pas : c’est tout l’intérêt de Sigma.