Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Semgrep : analyse statique du code pour la sécurité

Par mis à jour le

Améliorer cette page

Semgrep est un scanner d'analyse statique (SAST) open source qui cherche dans votre code des constructions dangereuses à partir de règles écrites en YAML, lisibles par n'importe quel développeur. Ce guide vous mène de l'installation à une règle maison testée, puis à un job CI qui bloque réellement. Il s'adresse à un développeur ou un DevOps qui a compris le principe du SAST et veut un outil concret. Toutes les sorties ont été relevées avec Semgrep 1.178.0, dont une règle d'exemple que le test officiel a prise en défaut.

  • Installer Semgrep et lancer un premier scan de votre code.
  • Choisir les règles du registre (packs par langage et par framework).
  • Rédiger vos propres règles et les tester avant de les déployer.
  • Intégrer Semgrep dans un pipeline CI/CD sans bloquer les équipes.
  • Maîtriser les faux positifs et les exclusions, y compris leur piège.

Semgrep lit votre code source sans l'exécuter et cherche des motifs (patterns) qui correspondent à des vulnérabilités, des bugs ou des mauvaises pratiques. Son nom vient de « semantic grep » : comme grep, il cherche, mais il comprend la structure du code. Il sait qu'une variable est une variable et qu'un appel de fonction reste le même quelle que soit la mise en forme.

# Construction que Semgrep sait reconnaître : une requête SQL par concaténation
query = "SELECT * FROM users WHERE id = " + user_input
cursor.execute(query)

Le principe général du SAST, sources, sinks et suivi de flux, est présenté dans la leçon SAST. Ce guide se concentre sur l'outil.

Le principal atout de Semgrep est la lisibilité de ses règles : une règle tient en quelques lignes de YAML que l'on relit en revue de code, là où d'autres outils imposent un langage de requête dédié. Il est aussi rapide, de l'ordre de la seconde sur un petit dépôt. Le nombre de faux positifs, lui, dépend surtout du jeu de règles choisi : un registre trop large produit des doublons et des règles hors sujet, comme le montre le premier scan ci-dessous.

AspectSemgrep
RèglesFichiers YAML lisibles, testables avec semgrep --test
VitesseDe l'ordre de la seconde sur un petit dépôt
Langages30+
Faux positifsDépendent du jeu de règles : choisir celui de son framework
LicenceMoteur open source, LGPL 2.1
SortieTexte, JSON, SARIF, JUnit

Les règles du registre couvrent trois familles de problèmes. Les vulnérabilités d'abord : injections SQL et de commande, XSS, traversée de chemin (path traversal, l'accès à un fichier hors du répertoire prévu). Les problèmes cryptographiques ensuite : MD5 ou SHA-1 pour des mots de passe, générateurs aléatoires non sûrs, vérification TLS désactivée. Les mauvaises pratiques enfin, comme eval() ou des permissions trop larges.

Semgrep s'installe en une commande. En CI, préférez l'image officielle épinglée par digest ; sur le poste, n'importe quelle méthode convient tant que la version est fixée, sans quoi deux développeurs ne verront pas les mêmes résultats.

Semgrep 1.178.0 exige Python 3.10 ou plus récent (champ requires_python du paquet publié sur PyPI). pipx l'installe dans un environnement isolé, sans toucher au Python du système :

Fenêtre de terminal
pipx install semgrep==1.178.0
# Vérification
semgrep --version
# 1.178.0

Le premier scan sert surtout à comprendre la sortie et le bruit qu'elle contient. Le fichier de test de ce guide contient cinq appels à cursor.execute, dont quatre vulnérables (concaténation, %, .format(), f-string) et un paramétré, plus un eval().

Le mode --config auto choisit les règles d'après les langages détectés. Il a une condition peu connue : il exige l'envoi de métriques d'usage à l'éditeur. Mesuré : avec --metrics=off, Semgrep refuse de démarrer et sort en code 2 avec le message Cannot create auto config when metrics are off. Dans un environnement où la télémétrie est proscrite, nommez donc un pack explicite.

Fenêtre de terminal
semgrep scan --config auto .

Sur le fichier de test, le mode auto charge 1 074 règles et remonte 8 constats sur cinq lignes. Le eval() est bien signalé, mais les quatre requêtes vulnérables le sont par des règles SQLAlchemy alors que le code utilise sqlite3, avec deux règles pour chaque requête formatée. Le scan est utile, mais il illustre pourquoi un pack ciblé vaut mieux qu'un filet aussi large.

Chaque constat suit la même structure. Voici la sortie réelle de la règle eval présentée plus bas dans ce guide, lancée avec un message abrégé :

┌────────────────┐
│ 1 Code Finding │
└────────────────┘
app.py
❯❱ rules.dangerous-eval-usage
❰❰ Blocking ❱❱
eval() exécute du code arbitraire.
11┆ return eval(expr)

L'identifiant rules.dangerous-eval-usage est le nom de la règle, préfixé par le dossier qui la contient : c'est lui que vous citerez dans un nosemgrep, qui accepte aussi la forme courte, sans préfixe (mesuré). Le libellé Blocking indique que le constat ferait échouer un scan lancé avec --error. La ligne 11┆ donne l'emplacement exact, et le message est celui que l'auteur de la règle a rédigé.

Semgrep classe ses règles en trois sévérités, fixées par l'auteur de chaque règle. Elles servent à construire une politique de blocage graduée, détaillée dans la leçon SAST : on bloque d'abord sur ERROR, on trie le reste.

NiveauSignificationAction recommandée
ERRORProblème probablement exploitableCorriger avant la fusion
WARNINGProblème potentiel, à investiguerAnalyser et corriger si confirmé
INFOBonne pratique non respectéeAmélioration suggérée

L'option --severity ERROR limite le scan à une sévérité. Mesuré avec le pack p/python sur l'application Flask de la leçon SAST : 5 constats sans filtre, dont un WARNING, et 4 avec le filtre, tous en ERROR. C'est la façon la plus simple de commencer par un blocage restreint aux constats les plus graves, avant d'élargir.

Le mode auto dépanne pour débuter, mais un pack choisi rend le scan prévisible : vous savez quelles règles tournent, et les résultats ne changent pas selon les langages détectés.

Un pack est un ensemble de règles du registre regroupées par thème ou par langage, et maintenues par l'éditeur ou la communauté. Son identifiant commence par p/. Tous les packs cités ci-dessous ont été chargés sans erreur avec Semgrep 1.178.0.

Fenêtre de terminal
# Sécurité, toutes technologies
semgrep scan --config p/owasp-top-ten .
semgrep scan --config p/security-audit .
# Secrets (mots de passe, clés d'API)
semgrep scan --config p/secrets .
# Par langage
semgrep scan --config p/python .
semgrep scan --config p/javascript .
semgrep scan --config p/golang .
semgrep scan --config p/java .
# Pack pensé par l'éditeur pour la CI
semgrep scan --config p/ci .

Les packs se cumulent en répétant --config. Sur le fichier de test, les packs ne réagissent pas de la même façon : p/security-audit remonte un constat là où p/python n'en remonte aucun sur ce même code, alors que ce dernier en trouvait cinq sur l'application Flask de la leçon SAST. Un seul pack ne couvre donc pas tout, d'où l'intérêt d'en combiner deux, puis d'écrire vos propres règles.

Fenêtre de terminal
semgrep scan --config p/security-audit --config p/secrets .

Le registre public se parcourt sur semgrep.dev/explore, par langage, catégorie ou mot-clé. Chaque règle y montre son code source, ce qui permet de vérifier ce qu'elle cherche avant de l'activer. Le préfixe r/ désigne une règle individuelle, un chemin local désigne vos propres fichiers.

Fenêtre de terminal
# Une seule règle du registre
semgrep scan --config "r/python.lang.security.audit.dangerous-system-call" .
# Vos règles, dans un dossier du dépôt
semgrep scan --config rules/ .

Les règles du registre ne connaissent ni vos fonctions internes ni vos conventions : une règle maison détecte l'appel interdit dans votre base de code, par exemple une fonction d'accès aux données dépréciée. C'est la force principale de Semgrep, et aussi son principal risque : une règle fausse donne une fausse assurance. D'où la section sur les tests, qui n'est pas optionnelle.

Voici une règle qui détecte eval() en Python. Mesuré sur le fichier de test, elle remonte bien la ligne return eval(expr) :

rules/eval.yaml
rules:
- id: dangerous-eval-usage
patterns:
- pattern: eval($X)
message: |
Utilisation de eval() détectée. La fonction exécute du code Python
arbitraire : si l'argument provient d'une entrée utilisateur, un
attaquant peut exécuter n'importe quel code sur le serveur.
Alternatives : ast.literal_eval() pour des structures simples,
json.loads() pour du JSON.
severity: WARNING
languages:
- python
metadata:
category: security
cwe: "CWE-95: Improper Neutralization of Directives"

Chaque champ a un rôle précis, et cinq d'entre eux sont obligatoires. Le $X est une métavariable : elle capture n'importe quelle expression, que la règle peut ensuite réutiliser ou contraindre.

ChampObligatoireDescription
idOuiIdentifiant unique de la règle
patterns ou patternOuiCe que Semgrep doit chercher dans le code
messageOuiExplication affichée quand la règle correspond
severityOuiINFO, WARNING ou ERROR
languagesOuiListe des langages concernés
metadataNonInformations supplémentaires (CWE, références)

Une règle d'injection SQL, et le défaut que le test a trouvé

Section intitulée « Une règle d'injection SQL, et le défaut que le test a trouvé »

Cette règle cherche les quatre façons de construire une requête SQL à partir d'une chaîne. Une version antérieure de ce guide ajoutait deux exclusions pattern-not: cursor.execute($QUERY, ...) pour écarter les requêtes paramétrées. Mesuré avec semgrep --test : cette version manquait le cas f-string, celui-là même que son message citait en exemple. Les exclusions étaient inutiles, puisqu'aucun des motifs positifs ne correspond à un appel à deux arguments, et nuisibles.

rules/sql-injection.yaml
rules:
- id: sql-injection-python
pattern-either:
# Formatage avec %
- pattern: cursor.execute($QUERY % ...)
# Formatage avec .format()
- pattern: cursor.execute($QUERY.format(...))
# f-string
- pattern: cursor.execute(f"...")
# Concaténation
- pattern: cursor.execute($QUERY + ...)
message: |
Requête SQL construite par concaténation ou formatage de chaîne :
un attaquant peut y injecter du SQL. Utilisez une requête paramétrée,
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)).
severity: ERROR
languages:
- python
metadata:
category: security
cwe: "CWE-89: SQL Injection"

Mesuré sur le fichier de test, cette version remonte les quatre lignes vulnérables et ignore la requête paramétrée. Les opérateurs se combinent : pattern-either correspond si l'un des motifs est présent, pattern-not exclut un cas, patterns exige que tous les motifs listés soient vrais.

semgrep --test est le test unitaire des règles. On place à côté de la règle un fichier de code du même nom, annoté : # ruleid: <id> au-dessus de chaque ligne qui doit être signalée, # ok: <id> au-dessus de celle qui ne doit pas l'être.

rules/sql-injection.py
def lire(cursor, user_id):
# ruleid: sql-injection-python
cursor.execute("SELECT * FROM users WHERE id = " + user_id)
# ruleid: sql-injection-python
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# ok: sql-injection-python
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
Fenêtre de terminal
semgrep --test rules/

Sur l'ancienne version de la règle, le test échoue et nomme la ligne manquée : missed lines: [5], la f-string. Sur la version corrigée, il affiche 1/1: ✓ All tests passed et sort en code 0. Lancez ce test en CI sur le dossier rules/ : une règle modifiée sans son test est un contrôle que plus personne ne vérifie. Le playground en ligne reste pratique pour une première ébauche, mais il ne remplace pas un test versionné.

Semgrep prend sa valeur quand il tourne à chaque modification de code et peut bloquer la fusion. L'ancienne action semgrep-action est dépréciée par son éditeur, qui recommande de lancer Semgrep dans son image officielle : c'est ce que font les exemples ci-dessous. La place du SAST dans la chaîne et la stratégie de blocage sont traitées dans la leçon SAST.

Ce workflow scanne chaque pull request et chaque push sur main, puis publie le rapport SARIF dans l'onglet Security du dépôt. permissions: {} retire tous les droits par défaut ; le job ne reçoit que contents: read pour cloner et security-events: write pour l'envoi SARIF.

.github/workflows/semgrep.yml
name: Semgrep
on:
pull_request:
push:
branches:
- main
permissions: {}
jobs:
semgrep:
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write
container:
image: semgrep/semgrep:1.178.0@sha256:32e459968daabe7ab86968184a29109b9564aa00392401156f9788452b42786b
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Scan Semgrep
run: semgrep scan --config p/security-audit --error --metrics=off --sarif --output semgrep.sarif .
- name: Envoi SARIF vers l'onglet Security
if: always()
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
sarif_file: semgrep.sarif

Trois options portent le contrôle. --error fait échouer le job dès qu'un constat existe : sans elle, le scan sort en code 0 quoi qu'il trouve. --sarif --output écrit le rapport, et if: always() l'envoie même quand le scan a échoué, c'est-à-dire précisément quand il a trouvé quelque chose.

Sur GitLab, le job tourne dans l'image officielle épinglée par digest, sur les merge requests et la branche par défaut. Le rapport JSON est conservé une semaine en artefact, même en cas d'échec, pour l'analyse.

.gitlab-ci.yml
semgrep:
stage: test
image: semgrep/semgrep:1.178.0@sha256:32e459968daabe7ab86968184a29109b9564aa00392401156f9788452b42786b
script:
- semgrep scan --config p/security-audit --error --metrics=off --json --output semgrep.json .
artifacts:
paths:
- semgrep.json
when: always
expire_in: 1 week
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Le hook pre-commit donne un retour avant même le commit. Les hooks officiels vivent désormais dans le dépôt semgrep/pre-commit, versionné comme l'outil. Le hook ne reçoit que les fichiers modifiés, ce qui le garde rapide.

.pre-commit-config.yaml
repos:
- repo: https://github.com/semgrep/pre-commit
rev: v1.178.0
hooks:
- id: semgrep
args: ["--config", "p/ci", "--error", "--skip-unknown-extensions"]
Fenêtre de terminal
pipx install pre-commit
pre-commit install

Chaque git commit lance alors le scan, et un constat bloque le commit. Le hook reste contournable d'un git commit --no-verify : il sert le confort du développeur, la CI reste la garantie. Sur un gros dépôt, gardez au hook un pack léger comme p/ci et réservez les packs larges à la CI.

Un faux positif est un constat sur du code qui n'est pas réellement exploitable. Les trois mécanismes ci-dessous vont du plus ciblé au plus large ; le plus ciblé est presque toujours le bon, parce qu'une exclusion large désactive aussi la détection des vraies failles qui arriveront au même endroit.

Le commentaire nosemgrep se pose sur la ligne rapportée par le constat, en nommant la règle. Mesuré dans la leçon SAST : avec l'identifiant exact, le constat disparaît ; avec celui d'une autre règle, il reste. Un nosemgrep nu éteint toutes les règles de la ligne, à réserver aux cas justifiés en revue.

# L'expression vient d'une liste blanche validée par parse_expression()
result = eval(validated_expression) # nosemgrep: rules.dangerous-eval-usage

Ajoutez toujours un commentaire qui dit pourquoi l'alerte est ignorée : c'est lui que relira la personne qui hérite du code dans un an.

Le fichier .semgrepignore, à la racine du dépôt, exclut des chemins avec la syntaxe d'un .gitignore. Son piège est mesuré : sans ce fichier, Semgrep applique une liste d'exclusion par défaut qui écarte déjà tests/ ; dès qu'il existe, il remplace cette liste. Un fichier vide fait tout scanner, un fichier qui ne liste que generated/ fait soudain scanner les tests. Reprenez donc explicitement les exclusions que vous voulez garder.

.semgrepignore
# Tests (la liste par défaut les excluait : on garde ce choix)
tests/
*_test.py
test_*.py
# Code généré
generated/
*.pb.go
# Dépendances vendorisées
vendor/
node_modules/
third_party/

L'option --exclude écarte un chemin le temps d'un scan, sans toucher au .semgrepignore partagé. Mesuré : --exclude qa retire bien le dossier qa/ des résultats. Elle sert à un diagnostic ponctuel ; une exclusion durable appartient au fichier versionné, où elle se relit en revue.

Fenêtre de terminal
semgrep scan --config p/security-audit --exclude "tests" --exclude "*.test.js" .

Activer Semgrep en mode bloquant du jour au lendemain sur un dépôt existant fait échouer chaque merge request à cause de la dette accumulée, et l'équipe finit par désactiver le contrôle. Le déploiement se fait donc par paliers, chacun avec un critère de sortie mesurable.

  1. Observer : lancez un scan complet sans bloquer, et comptez.

    Fenêtre de terminal
    semgrep scan --config p/security-audit --metrics=off --json --output rapport-initial.json .
    python3 -c "import json;print(len(json.load(open('rapport-initial.json'))['results']), 'constats')"

    Triez un échantillon : combien de vraies failles, combien de bruit ? Le résultat dit s'il faut changer de pack avant d'aller plus loin.

  2. Bloquer les nouveautés : en CI, ne bloquez que les constats introduits par la merge request, avec --baseline-commit. Mesuré dans la leçon SAST : la dette est ignorée, la nouvelle faille fait échouer le job.

    Fenêtre de terminal
    semgrep scan --config p/security-audit --error --baseline-commit "$(git merge-base origin/main HEAD)" .
  3. Résorber la dette : traitez le rapport initial dans un backlog daté, avec un scan complet planifié, sinon la baseline devient un angle mort.

  4. Bloquer tout : quand la dette est résorbée, retirez la baseline. Chaque constat bloque, et chaque exception passe par un nosemgrep justifié.

Semgrep écrit ses résultats dans le format qui sert le lecteur du rapport. Le texte sert au terminal, le JSON aux scripts, le SARIF aux tableaux de bord de sécurité, le JUnit aux outils de CI qui affichent des tests en échec.

Fenêtre de terminal
# Texte (défaut)
semgrep scan --config p/security-audit .
# JSON : tous les détails, pour un traitement automatisé
semgrep scan --config p/security-audit --json --output resultats.json .
# SARIF 2.1.0 : onglet Security de GitHub, tableau de bord GitLab
semgrep scan --config p/security-audit --sarif --output resultats.sarif .
# JUnit : un constat devient un test en échec
semgrep scan --config p/security-audit --junit-xml --output resultats.xml .

Mesuré : le JUnit produit un testsuite nommé semgrep results où chaque constat compte comme un échec, et le SARIF déclare l'outil Semgrep OSS en version 2.1.0 du format.

Tout ce guide utilise le moteur open source, qui couvre l'essentiel des besoins d'une équipe : scanner complet, registre public, règles maison, tous les formats de sortie et l'intégration CI. L'éditeur commercialise par ailleurs la Semgrep AppSec Platform, qui ajoute principalement trois choses :

  • une analyse inter-fichiers et des règles propriétaires plus précises ;
  • des produits complémentaires, Supply Chain (analyse des dépendances) et Secrets (détection avec validation) ;
  • un tableau de bord centralisé : triage, assignation, historique, intégrations Jira et Slack, authentification unique.

Les tarifs et les limites de l'offre gratuite changent régulièrement : consultez la page de l'éditeur au moment de décider. Le passage à la version payante se justifie quand vous gérez des dizaines de dépôts, qu'un audit exige un historique des constats, ou que le volume de faux positifs du registre public coûte plus que les licences.

Semgrep reste un outil d'analyse statique, avec les angles morts de la famille. Il ne voit pas ce qui n'apparaît qu'à l'exécution : c'est le rôle du DAST. Il ne cherche pas les vulnérabilités connues de vos dépendances : c'est le rôle de la SCA. Et le moteur open source suit les données à l'intérieur d'un fichier : un flux qui traverse plusieurs fichiers échappe aux règles de taint de cette édition.

La couverture varie aussi selon les langages : Python, JavaScript, Go et Java ont beaucoup de règles, des langages moins courants beaucoup moins. Enfin, les règles maison sont du code : elles se testent, se relisent et se maintiennent quand la base de code évolue, faute de quoi elles cessent de détecter ce qu'elles promettent.

  1. Semgrep cherche des motifs dans le code à partir de règles YAML lisibles, sans l'exécuter.
  2. Le mode --config auto exige les métriques : avec --metrics=off, nommez un pack.
  3. Un pack ciblé, p/<langage> ou p/ci, rend le scan prévisible ; un filet trop large produit des doublons.
  4. Une règle maison se teste avec semgrep --test et des annotations ruleid: et ok:.
  5. Sans --error, le scan sort en code 0 et ne bloque rien.
  6. Un .semgrepignore remplace la liste d'exclusion par défaut : reprenez tests/ si vous voulez l'exclure.
  7. On déploie par paliers : observer, bloquer les nouveautés avec --baseline-commit, résorber, bloquer tout.
  8. Semgrep complète le DAST et la SCA, il ne les remplace pas.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn