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

TP 2 — Alerting & Notification Discord

Acte 2 · SIEM Avancé Grafana Alerting · Discord · rejeu d'incident


📡 Poste SOC · La Réunion — Acte 2, Détecter : vous armez Alfred Faure (Crozet), ~2 900 km. Liaison SATCOM nominale.

Une détection ne sert à rien si personne ne la voit. On ferme la boucle : règle Sigma → requête LogQL → alerte Grafana → notification Discord — sur la même stack Grafana, sans composant externe.

La règle existe (TP 1) mais elle dort. Le jour J, aucune alerte n’a jamais tonné — personne n’avait fermé la boucle. Ici vous la fermez : vous déclenchez l’attaque depuis votre poste, et vous regardez la notification tomber dans Discord. C’est la seconde qui aurait réveillé l’astreinte ce jour-là.

  • Comprendre la chaîne règle ⇒ alert rule ⇒ notification policy ⇒ contact point.
  • Provisionner une alerte par YAML (alerting-as-code) — pas par clics dans l’UI.
  • Configurer des notification policies par sévérité (Discord channel routing).
  • Déclencher l’attaque en rejouant l’incident (regen-logs.sh) et voir la notification tomber.
  • Tuner les faux positifs contre le bruit de fond du log-streamer.
graph LR
 subgraph "Détection-as-Code (TP1)"
 RULE[/règle.yml<br/>Sigma/]
 end

 subgraph "Compilation"
 SIGMA[sigma convert<br/>-f grafana_alerting]
 end

 subgraph "Grafana Alerting"
 PROV[/provisioning/<br/>alert-rules.yml/]
 AR[Alert Rule]
 NP[Notification Policy<br/>severity routing]
 CP[Contact Point<br/>Discord webhook]
 end

 subgraph "Sortie"
 DISCORD([Discord<br/>#soc-critical<br/>#soc-info])
 end

 RULE --> SIGMA
 SIGMA --> PROV
 PROV --> AR
 AR -->|severity label| NP
 NP --> CP
 CP --> DISCORD

 classDef code fill:#99ff99,stroke:#333,stroke-width:2px
 classDef alert fill:#ffccff,stroke:#333,stroke-width:2px
 classDef out fill:#ffcc99,stroke:#333,stroke-width:2px

 class RULE,SIGMA,PROV code
 class AR,NP,CP alert
 class DISCORD out

Vous arrivez avec un repo taaf-detections contenant au moins :

  • rules/initial_access/vpn-auth-external-ip.yml
  • rules/collection/falco-classified-vault-read.yml
  • rules/exfiltration/falco-pgdump-genetic-samples.yml
  • scripts/compile.sh
  1. Discord → votre serveur de TP → Paramètres du salonIntégrationsCréer un webhook.
  2. Nommez-le TAAF-SOC-bot, choisissez le salon #soc-critical (créez-le si besoin).
  3. Copiez l’URL du webhook : https://discord.com/api/webhooks/<id>/<token>.

Vérifiez :

Fenêtre de terminal
curl -X POST -H "Content-Type: application/json" \
-d '{"content":"Hello from TAAF SOC 👋"}' \
"$DISCORD_WEBHOOK_CRITICAL"

→ Un message doit apparaître dans #soc-critical.

Créez aussi #soc-info (avec son propre webhook → $DISCORD_WEBHOOK_INFO).

  • Le générateur du scénario (dépôt data, joué par regen-logs.sh en Acte 0 Phase 4) — c’est lui qui réinjecte la kill chain datée maintenant, sur laquelle vos alertes se déclenchent.

3. Étape 1 — Webhook Discord + Contact Point Grafana

Section intitulée « 3. Étape 1 — Webhook Discord + Contact Point Grafana »

Ouvrez Grafana (http://IP_VM1:3000) → AlertingContact points+ Add contact point.

ChampValeur
Namediscord-soc-critical
IntegrationDiscord
Webhook URL<votre webhook critical>
Use Discord usernameTAAF-SOC-bot
Avatar URL(laissez vide ou mettez un logo TAAF)
Message Content{{ template "discord.taaf.message" . }}
Use template for message✅ (on créera le template en 3.3)

Test : bouton Test → vous devez voir un message arriver dans #soc-critical.

Recommencez pour discord-soc-info (webhook info).

3.2 — Provisionner par YAML (recommandé pour la traçabilité)

Section intitulée « 3.2 — Provisionner par YAML (recommandé pour la traçabilité) »

Plutôt que de cliquer, déposez dans le repo monitoring le fichier grafana/provisioning/alerting/contact-points.yml :

apiVersion: 1
contactPoints:
- orgId: 1
name: discord-soc-critical
receivers:
- uid: discord_critical
type: discord
settings:
url: ${DISCORD_WEBHOOK_CRITICAL}
use_discord_username: true
username: TAAF-SOC-bot
message: |
{{ template "discord.taaf.message" . }}
- orgId: 1
name: discord-soc-info
receivers:
- uid: discord_info
type: discord
settings:
url: ${DISCORD_WEBHOOK_INFO}
username: TAAF-SOC-bot
message: |
{{ template "discord.taaf.message" . }}

Et dans .env du compose Grafana :

Fenêtre de terminal
DISCORD_WEBHOOK_CRITICAL=https://discord.com/api/webhooks/.../...
DISCORD_WEBHOOK_INFO=https://discord.com/api/webhooks/.../...
Fenêtre de terminal
docker compose restart grafana

Dans Grafana → AlertingContact pointsNotification templates+ Add template :

{{ define "discord.taaf.message" }}
🚨 **{{ .CommonLabels.alertname }}** — {{ .CommonLabels.severity | toUpper }}
{{ range .Alerts }}
**État :** {{ .Status }}
**Règle :** `{{ .Labels.rulename }}` (ATT&CK : {{ .Labels.attack_id }})
**Source :** {{ .Labels.job }}
**Détails :** {{ .Annotations.summary }}
**Lien :** [Explore]({{ .GeneratorURL }})
{{ end }}
{{ end }}

Reprenez le repo taaf-detections du TP1.

4.1 — Conversion avec le format grafana_alerting

Section intitulée « 4.1 — Conversion avec le format grafana_alerting »
Fenêtre de terminal
cd ~/taaf-detections
source ~/sigma-venv/bin/activate
sigma convert \
-t loki \
-f grafana_alerting \
-p loki_grafana \
rules/initial_access/vpn-auth-external-ip.yml \
> build/alerts/vpn-auth-external-ip.yml

Sortie attendue (extrait) :

apiVersion: 1
groups:
- orgId: 1
name: taaf-detections
folder: SOC-TAAF
interval: 1m
rules:
- uid: vpn-auth-external-ip
title: VPN Authentication from External IP (TAAF)
condition: A
data:
- refId: A
datasourceUid: loki
model:
expr: |
{job="auth"} |= `WARNING` |= `external IP`
noDataState: OK
execErrState: Error
for: 1m
labels:
severity: high
attack_id: T1078
attack_id_secondary: T1133
annotations:
summary: "Auth VPN externe pour utilisateur TAAF"
description: "Auth VPN réussie depuis IP non-interne, non-partenaire."
#!/usr/bin/env bash
set -euo pipefail
OUT_LOGQL="build/logql"
OUT_ALERTS="build/alerts"
mkdir -p "$OUT_LOGQL" "$OUT_ALERTS"
find rules -name "*.yml" -type f | while read -r rule; do
name=$(basename "$rule" .yml)
echo "[+] $rule"
sigma convert -t loki -p loki_grafana "$rule" > "$OUT_LOGQL/$name.logql"
sigma convert -t loki -f grafana_alerting -p loki_grafana "$rule" > "$OUT_ALERTS/$name.yml"
done
echo "[✓] $(ls "$OUT_LOGQL" | wc -l) LogQL · $(ls "$OUT_ALERTS" | wc -l) alertes Grafana"
Fenêtre de terminal
./scripts/compile.sh

5.1 — Déposer les alertes dans le provisioning Grafana

Section intitulée « 5.1 — Déposer les alertes dans le provisioning Grafana »

Copiez build/alerts/*.yml dans le volume monté par Grafana :

Fenêtre de terminal
# Depuis le repo monitoring
mkdir -p grafana/provisioning/alerting/rules
cp ~/taaf-detections/build/alerts/*.yml grafana/provisioning/alerting/rules/
# Redémarrer Grafana pour charger
docker compose restart grafana
docker compose logs -f grafana | grep -i alert

Vérification : Grafana → AlertingAlert rules → dossier SOC-TAAF → 3 règles visibles.

Dans l’UI de l’alerte → Preview → exécute la query et montre les résultats actuels. Si vos logs du jour J sont peuplés, la query doit retourner ≥ 1 ligne → l’alerte passera en Pending puis Firing après for: 1m.


6. Étape 4 — Notification policies par sévérité

Section intitulée « 6. Étape 4 — Notification policies par sévérité »

Grafana → AlertingNotification policiesDefault policy+ New nested policy :

ChampValeur
Matching labelsseverity = critical
Contact pointdiscord-soc-critical
Group byalertname, attack_id
Group wait15s
Group interval1m
Repeat interval4h

Recommencez :

| severity = high | discord-soc-critical | | severity = medium | discord-soc-info | | severity = low | discord-soc-info |

grafana/provisioning/alerting/notification-policies.yml :

apiVersion: 1
policies:
- orgId: 1
receiver: discord-soc-info # default
group_by: [alertname]
group_wait: 30s
group_interval: 5m
repeat_interval: 12h
routes:
- receiver: discord-soc-critical
matchers:
- severity =~ "critical|high"
group_by: [alertname, attack_id]
group_wait: 15s
group_interval: 1m
repeat_interval: 4h
- receiver: discord-soc-info
matchers:
- severity =~ "medium|low"
Fenêtre de terminal
docker compose restart grafana
Mauvaise pratiqueSymptôme
Tout en severity: criticalL’astreinte se déconnecte au bout d’1 semaine
Pas de group_by50 alertes pour 1 incident — analyste noyé
repeat_interval: 1mSpam Discord, on désactive le webhook
Pas de silences pour la maintenanceFaux positifs pendant les déploiements

→ Documentez votre politique d’alerting dans le README.md du repo taaf-detections.


Le déclencheur, c’est le rejeu. Vous ne fabriquez pas de faux événements à la main : le générateur réinjecte la kill chain complète datée à l’instant présent. Vos règles, évaluées sur la fenêtre récente, tombent alors pour de vrai.

Sur la VM apps (où vivent Alloy et le générateur) :

Fenêtre de terminal
cd ~/monitoring
./regen-logs.sh 2026-A # réinjecte l'incident (auth, windows, falco, zeek, nextcloud…) daté maintenant

Puis, dans les ~2 minutes (intervalle d’évaluation Grafana), les alertes correspondantes passent Firing et tombent dans Discord :

Signal rejouéRègle qui tombeLogQL de l’alerte
Auth VPN externe (T1078)vpn-auth-external-ip{job="auth"} |= "WARNING" |= "external IP"
Accès coffre (T1213)falco-classified-vault-read{job="falco"} | json | rule="Read sensitive file untrusted" |= "CLASSIFIED-VAULT-X"
pg_dump exfil (T1041)falco-pgdump-genetic-samples{job="falco"} |= "pg_dump"

📸 Capture #1 : Grafana → Alert state Firing. 📸 Capture #2 : message Discord avec le template rendu (severity=critical, mention du signal).


8.1 — Le bruit de fond bénin est déjà là (log-streamer)

Section intitulée « 8.1 — Le bruit de fond bénin est déjà là (log-streamer) »

Vous n’avez pas à fabriquer le bruit : le service log-streamer de la Phase 4 (profil scenario) réinjecte en continu de l’activité bénigne fraîche dans les cinq catégories (auth, réseau, satcom, système, falco). C’est sur ce bruit que vos règles doivent rester silencieuses — sinon elles crient au loup.

Fenêtre de terminal
# Sur la VM apps — vérifier que le streamer tourne
docker compose -f docker-compose-apps.yml ps log-streamer

Laissez-le tourner pendant que vous observez vos alertes.

Vérifiez #soc-info : combien d’alertes medium/low sont tombées sans qu’il y ait eu d’attaque ?

Ajoutez des filter_* dans les règles Sigma. Exemple — la règle pg_dump ne doit pas s’affoler sur un dump de maintenance planifié : on exclut la fenêtre de sauvegarde légitime (le job tourne la nuit, jamais depuis un shell interactif) :

detection:
keywords:
- 'pg_dump'
filter_maintenance:
output|contains: 'cron' # le backup planifié passe par cron, pas par 'sh -c'
condition: keywords and not filter_maintenance

Recompilez (./scripts/compile.sh), redéployez (docker compose restart grafana), laissez tourner le bruit de fond (le log-streamer de la Phase 4) — comparez le nombre de FP avant/après.

RègleFP avant tuningCause FPFiltre ajoutéFP après tuning
falco-pgdump-genetic-samples8 / heuredump de maintenance planifiéfilter_maintenance_window0
vpn-auth-external-ip00
falco-classified-vault-read2 / heureBackup nightlyvault_authorized_users += backup-cron0

À mentionner dans votre rapport : un paragraphe de ~10 lignes sur le pourquoi du choix Discord (pédagogique) et le plan de migration vers un canal souverain pour la prod.


  1. Repo taaf-detections enrichi :
    • build/alerts/*.yml (3 alertes Grafana compilées)
    • scripts/compile.sh étendu (LogQL + alerts)
    • README.md avec la politique d’alerting (sévérités, groupings, repeat intervals)
  2. Repo monitoring enrichi :
    • grafana/provisioning/alerting/contact-points.yml
    • grafana/provisioning/alerting/notification-policies.yml
    • grafana/provisioning/alerting/rules/*.yml (3 fichiers copiés du build)
    • grafana/templates/discord.taaf.tmpl (notification template)
  3. 3 captures :
    • Grafana Alert state Firing
    • Message Discord #soc-critical (template rendu)
    • Tableau Notification policies
  4. Tableau de tuning FP (markdown ou PDF) — § 8.4
CritèrePondération
Webhook Discord configuré et testé (test message reçu)10 %
Contact points provisionnés par YAML (pas par clics)15 %
Compilation Sigma → grafana_alerting fonctionnelle (3 alertes)20 %
Notification policies par sévérité (≥ 2 routes)15 %
Au moins une attaque déclenchée fait tomber une notification Discord20 %
Tableau de tuning FP rempli avec valeurs réelles10 %
Paragraphe souveraineté + plan de migration10 %

Format de rendu : PDF nom-prenom-promo.pdf sur Moodle.


(Voir § 6.2 — vérifier qu’elle est strictement identique chez l’étudiant.)

  • Webhook hardcodé dans contact-points.yml (sans ${DISCORD_WEBHOOK_CRITICAL}) → push d’un secret sur Git. -15 %.
  • Pas de group_by → la promo a découvert l’effet en TP : 30+ notifications pour 1 attaque. -10 %.
  • for: à 0s → l’alerte tombe puis se résout aussitôt (oscillation). Doit être ≥ 1 min. -5 %.
  • Sévérité non cohérente avec la règle Sigma (level: critical dans Sigma mais severity: medium dans le label Grafana) → -5 %.

À la fin du TP2, l’étudiant a 3 alertes provisionnées qui tombent quand l’attaque est rejouée. Au TP3, on va mesurer la couverture ATT&CK de ces règles (S2AN → Navigator) et construire une corrélation multi-étapes (phishing → puis → escalade → puis → exfil) pour reconstruire la kill chain, pas juste voir des alertes éparses.