Aller au contenu
TAAF-OPS --:-- UTC

TP 1 — Détection as Code : Sigma → LogQL

Acte 2 · SIEM Avancé Sigma · pySigma · LogQL · MITRE ATT&CK

Auteur : Thibaut Fontaine Institution : Université de la Réunion Date : 2026



📡 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…).

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.

  • 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-loki et 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).
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

Fenêtre de terminal
vagrant ssh soc
# Vérifier que Python 3 est dispo
python3 --version # Python 3.10+ attendu
# Installer Sigma CLI + le backend Loki
python3 -m venv ~/sigma-venv
source ~/sigma-venv/bin/activate
pip install --upgrade pip
pip install sigma-cli==1.0.* pysigma-backend-loki==0.13.* pysigma-pipeline-loki==0.13.*
sigma version # → sigma-cli 1.0.x
sigma plugin list # → loki backend, loki pipeline (installés)

Depuis la VM soc (joint la VM monitoring par IP) :

Fenêtre de terminal
curl -s http://IP_VM2:3100/ready # → ready
curl -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 ».

Dans Grafana Explore (datasource Loki), cliquez sur Label filters > job :

job attenduSource
falcoÉvénements Falco (runtime sensitive file, exec, network)
authLogs auth VPN + AD (02_auth_vpn_ad.log)
postgresLogs PostgreSQL base_af
windowsWindows Event Logs (Sysmon + Security)
zeek_ssl / zeek_conn / zeek_arpZeek (réseau, downgrade PQC, MitM)
sat_telemetryTé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).

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.

Sur la VM soc :

Fenêtre de terminal
mkdir -p ~/taaf-detections/rules
cd ~/taaf-detections
git init
mkdir -p rules/{initial_access,credential_access,collection,exfiltration}
echo "*.pyc" > .gitignore

4.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-5a6f7c8d9e10
status: experimental
description: |
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 SOC
date: 2026/06/16
logsource:
product: vpn
service: auth
detection:
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
- service
falsepositives:
- Utilisateur en déplacement déclaré (vérifier dans le calendrier RH)
- Nouvelle IP partenaire pas encore ajoutée à filter_known
level: high
tags:
- attack.initial_access
- attack.t1078
- attack.t1133

Lecture 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 :

Fenêtre de terminal
sudo systemctl reload falco-modern-bpf
journalctl -u falco-modern-bpf -n 20 # vérifier que la règle est bien chargée

Puis la règle Sigma côté SIEM (rules/collection/falco-classified-vault-read.yml) :

title: Falco — Unauthorized Read of CLASSIFIED Vault
id: a4f7c2d1-9e8b-4a3f-b6c5-1d2e3f4a5b6c
status: stable
description: 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 SOC
date: 2026/06/16
logsource:
product: falco
detection:
selection:
rule: 'Unauthorized Read of CLASSIFIED Vault'
priority: 'Critical'
condition: selection
fields:
- output_fields.user.name
- output_fields.proc.name
- output_fields.fd.name
level: critical
tags:
- attack.collection
- attack.t1213

4.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' et approved_by: NULL
  • tag : attack.exfiltration + attack.t1041

(Corrigé en Annexe.)


Fenêtre de terminal
cd ~/taaf-detections
source ~/sigma-venv/bin/activate
sigma convert -t loki -p loki_grafana_logfmt \
rules/initial_access/vpn-auth-external-ip.yml

Sortie 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)$`

Créez scripts/compile.sh :

#!/usr/bin/env bash
set -euo pipefail
OUT="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"
done
echo "[✓] $(ls "$OUT" | wc -l) règles compilées"
Fenêtre de terminal
chmod +x scripts/compile.sh
./scripts/compile.sh

Sortie 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ées

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.


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=a7f3c9e1

Pourquoi cette ligne tombe sur la règle :

  • service=vpn ✓ matche selection.service
  • auth_status=success ✓ matche selection.auth_status
  • src_ip=203.0.113.42 ne commence ni par 10.42. ni par 192.168. (pas dans filter_internal) et n’est pas dans filter_known → l’IP passe le not (filter_*) → l’événement est gardé

📸 Capture obligatoire : screenshot de Grafana Explore montrant la détection. Annotez-y le timestamp et l’IP suspecte.

{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).

À 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=null

Pourquoi cette ligne tombe sur la règle :

  • query contient genetic_samples
  • approved_by est null ✓ (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).


Fenêtre de terminal
cd ~/taaf-detections
git add rules/ scripts/ .gitignore
git 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.git
git push -u origin main

PiègeSymptômeSolution
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 labelCardinalité explose, Loki devient lentMettre 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 filtresFaux positifs ou faux négatifsToujours selection and not (filter_X or filter_Y)
Pas de tags: attack.txxxxPas de couverture ATT&CK au TP3Tagger systématiquement (norme MITRE)
Règle sans falsepositives:Tunage impossible au TP2Lister les FP connus dès l’écriture

  1. Repo taaf-detections contenant :
    • rules/initial_access/vpn-auth-external-ip.yml
    • rules/collection/falco-classified-vault-read.yml
    • rules/exfiltration/postgres-genetic-samples-no-approval.yml
    • scripts/compile.sh exécutable
    • README.md listant les 3 règles avec un lien ATT&CK
  2. Règle Falco custom déposée dans le repo monitoring (falco/rules.d/taaf-classified-vault.yaml).
  3. 3 captures Grafana Explore (PNG) montrant chacune une détection avec timestamp visible.
CritèrePondé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ègles15 %
Falco rule custom déployée et visible dans journalctl -u falco-modern-bpf10 %
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é.


rules/exfiltration/postgres-genetic-samples-no-approval.yml :

title: PostgreSQL — Genetic Samples Access without Approval
id: f3a4b5c6-d7e8-4f9a-b0c1-d2e3f4a5b6c7
status: experimental
description: |
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 SOC
date: 2026/06/16
logsource:
product: postgresql
service: access_log
detection:
selection:
query|contains: 'genetic_samples'
approved_by: null
condition: selection
fields:
- user
- query
- approved_by
- db
falsepositives:
- Requête d'administration (DDL/DCL) sur la table — ne devrait jamais arriver en prod
level: critical
tags:
- attack.collection
- attack.t1213
- attack.exfiltration
- attack.t1041

LogQL générée :

{job="postgres"} | json | query=~`.*genetic_samples.*` | approved_by=``
  • condition: selection and selection_2 au lieu de condition: 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.txxxx sans le préfixe attack. → 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 %.

À 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.