📍 Guide principal du sujet : Pipeline SAST DAST SCA 2026 : architecture, outils et intégration CI/CD
Ce tutoriel approfondit l’écriture de règles Semgrep adaptées au code propre à votre équipe, en complément du registry public.
Pourquoi écrire ses propres règles Semgrep en 2026
Semgrep CE 1.161.0 (release du 22 avril 2026) embarque par défaut plus de 2 800 règles maintenues par la communauté, couvrant les vulnérabilités classiques d’OWASP Top 10 et les antipatterns courants pour 30 langages. Cette base est excellente comme filet général, mais elle reste générique : aucune règle communautaire ne sait que votre fonction db_query() interne attend un paramètre déjà échappé, ou que l’usage de internal.crypto.legacy_hash() est bannie depuis trois mois dans votre charte sécurité. C’est précisément ce vide que les règles custom remplissent.
Une règle Semgrep custom bien écrite a trois propriétés. Elle exprime une connaissance métier que seul votre code possède, ce qui la rend impossible à dupliquer chez un éditeur externe. Elle exécute en quelques millisecondes par fichier, donc elle peut tourner en pre-commit sans freiner le développeur. Et elle bloque ou alerte avec un message contextualisé qui explique non seulement quoi mais pourquoi, ce qui transforme l’outil en machine à transmettre les standards d’équipe. Ce tutoriel montre comment passer du quick-start à un repository de règles versionnées, intégré au pipeline CI et au pre-commit hook.
Prérequis
- Python 3.9+ ou Docker installé sur la machine.
- Git et un dépôt cible — n’importe quel langage parmi ceux supportés (Python, JavaScript/TypeScript, Go, Java, C#, Ruby, PHP, etc.).
- Une compréhension de base d’un AST (arbre syntaxique abstrait) — utile pour comprendre la philosophie pattern.
- Niveau attendu : intermédiaire en développement, avoir déjà écrit du YAML, à l’aise avec une expression régulière.
- Temps estimé : 75 minutes pour parcourir l’ensemble et publier 3 règles d’équipe.
Étape 1 — Installer Semgrep CE et lancer le scan de référence
Semgrep s’installe en deux temps. La CLI Python convient à un usage local et à un poste de développement ; l’image Docker simplifie l’intégration CI sans imposer une dépendance Python sur le runner. Les deux modes acceptent exactement la même syntaxe de règle, ce qui permet de prototyper en local et d’exécuter en CI sans frictions.
pip install semgrep
semgrep --version
cd /chemin/vers/projet
semgrep scan --config auto
Le mode --config auto sélectionne automatiquement les règles pertinentes selon les langages détectés et envoie une télémétrie anonyme à Semgrep ; pour un environnement strictement offline, préférer semgrep scan --config p/owasp-top-ten qui charge le pack OWASP local sans appel réseau. La sortie résume le nombre de fichiers analysés, le temps écoulé et la liste des findings avec leur sévérité (LOW, MEDIUM, HIGH, CRITICAL depuis la refonte 2024). Ce premier scan sert de baseline avant l’ajout des règles custom.
Étape 2 — Comprendre la philosophie pattern
Une règle Semgrep ne décrit pas un texte à matcher mais une structure syntaxique. Le moteur parse le code source en AST, normalise certaines constructions équivalentes (par exemple if (x == null) et if (null == x)), puis cherche les sous-arbres qui correspondent au pattern. Cette approche structurée capture des occurrences que le grep manquerait — variables renommées, espaces différents, ordre d’arguments inversé — sans se faire piéger par les commentaires ou les chaînes de caractères qui contiennent le motif littéral.
Les métavariables (préfixées par $) jouent le rôle de joker typé. Le pattern hashlib.md5($X) matche tous les appels MD5, quelle que soit la donnée passée — chaîne littérale, variable ou expression complexe. Le pattern os.system($CMD) avec une métavariable $CMD permet ensuite d’inspecter le contenu de l’argument. Trois conventions valent d’être mémorisées : $X matche n’importe quelle expression, $STR avec metavariable-pattern peut être contraint à un littéral chaîne, et l’ellipse ... représente une suite arbitraire d’instructions ou d’arguments.
Étape 3 — Écrire une première règle ciblant un antipattern interne
Imaginons que votre équipe a interdit l’usage de requests.get() sans timeout explicite : un appel sans timeout peut bloquer un worker pendant plusieurs minutes en cas de cible lente, jusqu’à épuiser le pool de connexions. Cette règle est l’archétype de la connaissance métier qui n’apparaît dans aucun registry. Créer un fichier rules/python/no-requests-without-timeout.yml à la racine du dépôt.
rules:
- id: no-requests-without-timeout
languages: [python]
severity: HIGH
message: |
Un appel `requests.get()` ou similaire sans `timeout` peut bloquer
indéfiniment un worker. Ajouter un `timeout=(3, 10)` (connexion,
lecture) au minimum.
metadata:
cwe: CWE-400
owasp: A05:2021 - Security Misconfiguration
author: equipe-secu
pattern-either:
- pattern: requests.get(...)
- pattern: requests.post(...)
- pattern: requests.put(...)
- pattern: requests.delete(...)
pattern-not: requests.$METHOD(..., timeout=$T, ...)
L’idée s’appuie sur la composition pattern-either + pattern-not : on capture tout appel à un verbe HTTP de la bibliothèque requests, puis on exclut explicitement les variantes qui contiennent un argument nommé timeout. Tester immédiatement avec semgrep scan --config rules/python/no-requests-without-timeout.yml. La sortie liste les lignes coupables avec un extrait de code, et le message inclut la justification CWE-400 (Resource Exhaustion) qui aide le développeur à comprendre la gravité.
Étape 4 — Écrire une règle taint pour une injection SQL custom
Le mode taint suit la propagation des données depuis une source non fiable jusqu’à un puits sensible. Il dépasse largement la portée d’un pattern statique : une variable peut être assignée, passée à plusieurs fonctions, transformée par des helpers internes ; tant qu’elle n’est pas explicitement assainie, le moteur la considère comme tainted. Cet exemple cible une fonction custom internal_db.query(sql) dont aucun outil externe ne soupçonne l’existence.
rules:
- id: internal-db-sql-injection
languages: [python]
severity: CRITICAL
mode: taint
message: |
Donnée non assainie venant d'une requête HTTP injectée directement
dans `internal_db.query()`. Utiliser `internal_db.query_safe(sql,
params)` qui passe les paramètres en bind variables.
metadata:
cwe: CWE-89
owasp: A03:2021 - Injection
pattern-sources:
- pattern: request.args.get(...)
- pattern: request.form.get(...)
- pattern: request.json[...]
pattern-sanitizers:
- pattern: html.escape(...)
- pattern: internal_security.escape_sql(...)
pattern-sinks:
- pattern: internal_db.query($SQL)
Trois listes structurent la règle. Les sources énumèrent les points d’entrée HTTP de Flask. Les sanitizers reconnaissent les helpers maison qui rendent une donnée sûre — l’équipe peut en ajouter au fur et à mesure de la montée en maturité. Les sinks ciblent la fonction interne sensible. Lancer le scan, puis observer que la règle ne déclenche que pour les chemins de données réels, ce qui réduit drastiquement les faux positifs par rapport à un grep sur internal_db.query.
Étape 5 — Ajouter un autofix pour les règles déterministes
Quand la correction se déduit du pattern, Semgrep peut proposer ou appliquer le fix automatiquement avec la clé fix:. Cette capacité transforme une règle d’interdiction en règle de migration douce, particulièrement utile lors d’un changement de bibliothèque ou de l’introduction d’une fonction de remplacement.
rules:
- id: replace-deprecated-md5-hash
languages: [python]
severity: MEDIUM
message: |
hashlib.md5() est déprécié pour tout usage cryptographique.
Utiliser hashlib.sha256() ou bcrypt selon le cas d'usage.
pattern: hashlib.md5($X)
fix: hashlib.sha256($X)
metadata:
cwe: CWE-327
L’application est manuelle (mode interactif --autofix --dryrun qui affiche les diffs sans modifier) ou automatique (semgrep scan --autofix qui réécrit les fichiers). Sur un projet vivant, privilégier le dryrun en CI pour générer un commentaire de PR avec les diffs proposés et laisser le développeur les accepter individuellement. L’autofix devient destructeur si le pattern matche un cas légitime que l’auteur n’avait pas anticipé ; toujours réviser avant de merger.
Étape 6 — Versionner les règles dans un repo dédié
Une fois trois ou quatre règles écrites, le bon réflexe est d’extraire les règles dans un dépôt Git séparé, partagé entre tous les projets de l’organisation. Cela évite la duplication, centralise la revue et permet une versioning indépendante. Structure recommandée :
semgrep-rules-equipe/
├── README.md
├── python/
│ ├── no-requests-without-timeout.yml
│ ├── internal-db-sql-injection.yml
│ └── replace-deprecated-md5-hash.yml
├── javascript/
│ ├── no-eval.yml
│ └── react-no-dangerouslysetinnerhtml.yml
├── tests/
│ ├── python/
│ │ └── no-requests-without-timeout/
│ │ ├── test.py
│ │ └── test.expected.json
└── .pre-commit-hooks.yaml
Chaque règle a son cas de test : un fichier test.py contenant à la fois des occurrences attendues (# ruleid: no-requests-without-timeout) et des contre-exemples (# ok: no-requests-without-timeout). La commande semgrep --test exécute le pack et garantit qu’aucune modification ne brise les règles existantes. Cette discipline transforme le repo de règles en projet logiciel à part entière, avec CI et pull request review obligatoire avant merge.
Étape 7 — Brancher Semgrep en pre-commit et en CI
Le bénéfice maximal s’obtient quand chaque commit local et chaque pull request distant passent par les règles. Pour le pre-commit, le framework pre-commit de Yelp orchestre cette exécution. Ajouter dans .pre-commit-config.yaml du dépôt cible :
repos:
- repo: https://github.com/semgrep/semgrep
rev: v1.161.0
hooks:
- id: semgrep
args: ['--config', 'https://raw.githubusercontent.com/votre-org/semgrep-rules-equipe/main', '--error']
L’option --error renvoie un code de sortie non-nul dès qu’un finding apparaît, ce qui bloque le commit. Pour la CI GitLab, créer un job dédié qui rejoue le scan sur le diff de la merge request avec semgrep scan --baseline-commit origin/main. Cette syntaxe ne signale que les nouvelles violations introduites, ce qui évite de noyer la PR sous la dette historique.
Étape 8 — Mesurer la qualité d’un pack de règles
Une règle qui produit beaucoup de faux positifs sera désactivée par les développeurs en quelques jours. Mesurer la qualité passe par trois indicateurs simples. Le taux d’acceptation compte la part des findings que les développeurs corrigent ou explicitement justifient avec un nosemgrep, par rapport à ceux qui restent sans réponse — au-dessus de 70 %, la règle est utile. Le temps de scan doit rester sous deux secondes pour 10 000 lignes, sans quoi la friction pre-commit devient insupportable. Enfin, le nombre de tickets de revue ouverts contre la règle elle-même est le signal le plus net : si une règle génère plus de plaintes que de fixes, la repenser ou la supprimer.
L’export des findings vers DefectDojo se fait via la sortie SARIF avec semgrep scan --sarif --output report.sarif, puis l’import dans DefectDojo via son API permet d’agréger Semgrep, Trivy et SonarQube dans un seul backlog de remédiation.
Erreurs fréquentes
| Symptôme | Cause | Solution |
|---|---|---|
| La règle ne match pas alors que le code semble identique | Le pattern utilise des espaces ou un opérateur normalisé différemment | Tester le pattern dans le playground en ligne semgrep.dev/playground avant de versionner |
| Trop de faux positifs sur une bibliothèque legacy | Pattern trop large, pas de pattern-not-inside |
Restreindre avec pattern-not-inside: def legacy_$F(...): ... pour exclure les contextes connus |
| Le mode taint ne propage pas à travers une fonction interne | La fonction n’est pas reconnue comme propagateur | Ajouter un pattern-propagators avec la signature de la fonction et la métavariable $X |
| L’autofix casse la syntaxe | Le fix ne préserve pas le contexte (parenthèses, virgules) | Tester systématiquement en --dryrun avant --autofix |
| Performance dégradée sur monorepo | Scan complet à chaque commit | Utiliser --baseline-commit pour ne scanner que le diff |
| Findings dupliqués entre Semgrep CE et le pack OWASP | Deux règles couvrent le même pattern | Désactiver explicitement avec --exclude-rule dans la commande |
FAQ
Comment justifier proprement un faux positif ? Ajouter un commentaire nosemgrep: id-de-la-regle sur la ligne précédant l’occurrence, idéalement avec une justification : # nosemgrep: no-requests-without-timeout - timeout géré par le décorateur retry(). La revue de code valide ou refuse la justification.
Semgrep CE supporte-t-il l’analyse cross-file (interprocédural global) ? Pas en édition Community : l’analyse cross-file (Pro Engine) est réservée à l’offre payante depuis la séparation 2024. Pour des cas critiques, combiner CE avec CodeQL qui couvre nativement l’interprocédural.
Faut-il publier ses règles publiquement sur le registry ? Pour les règles génériques (antipatterns langage, vulnérabilités classiques), oui — la communauté en bénéficie et améliore les règles par PR. Pour les règles métier qui révèlent une fonction interne sensible, garder privé.
Quelle différence avec ESLint ou Bandit pour Python ? ESLint cible le style et la qualité, sa syntaxe de plugin est plus lourde. Bandit cible la sécurité Python uniquement. Semgrep est multi-langage et son DSL pattern reste lisible pour un dev sans expertise compilation. Les trois peuvent cohabiter sans conflit.
Le mode taint coûte-t-il cher en performance ? Oui, environ 3 à 5 fois plus lent que le mode pattern. À réserver aux règles à forte valeur (injections, désérialisation), pas aux interdictions de style.
Tutoriels associés
- Auto-héberger SonarQube Community Build 26.4 sur un VPS Linux pas-à-pas — installation et configuration de l’outil SAST historique, complémentaire à Semgrep.
- Réduire les faux positifs SAST de manière disciplinée — méthodologie de triage applicable aux règles Semgrep custom.
- 🔝 Retour au guide principal : Pipeline SAST DAST SCA 2026 : architecture, outils et intégration CI/CD