ITSkillsCenter
Business Digital

Réduire les faux positifs SAST de manière disciplinée

13 min de lecture

📍 Guide principal du sujet : Pipeline SAST DAST SCA 2026 : architecture, outils et intégration CI/CD
Ce tutoriel donne une méthode disciplinée pour traiter les faux positifs SAST, condition nécessaire à la pérennité d’un programme DevSecOps.

Pourquoi les faux positifs sont l’ennemi numéro un d’un pipeline SAST

Les retours d’expérience publics convergent : les outils SAST génèrent typiquement entre 30 et 60 % de faux positifs sur du code applicatif réel selon la maturité du tuning et la stack ciblée. Cette friction explique l’observation maintes fois confirmée sur le terrain : la première année, l’équipe s’investit, trie, corrige ; à partir de la deuxième année, les alertes sont systématiquement ignorées et l’outil tourne dans le vide. Le coût d’un faux positif n’est pas seulement le temps perdu à le trier — il est surtout le crédit confiance entamé. Quand un développeur reçoit dix alertes dont sept sont fausses, il finit par ignorer les trois vraies aussi.

Réduire les faux positifs n’est pas une opération ponctuelle de configuration mais un processus continu. Cinq leviers cohabitent : tuner les règles existantes, écrire des règles plus spécifiques au code de l’équipe, supprimer les findings qui ne s’appliquent pas avec une justification tracée, mesurer le ratio FP par règle pour cibler les pires, et rejouer périodiquement la baseline pour vérifier que les suppressions restent valables. Ce tutoriel développe chaque levier avec des exemples concrets sur Semgrep, SonarQube et Trivy.

Prérequis

  • Au moins un outil SAST déjà en production sur votre dépôt depuis quelques semaines.
  • Une plateforme d’agrégation des findings (DefectDojo OSS, Faraday ou GitLab Vulnerability Report).
  • Accès en écriture aux fichiers de configuration des outils et au repo de règles.
  • Niveau attendu : intermédiaire DevSecOps, capacité à lire un rapport SARIF.
  • Temps estimé : 90 minutes pour traiter une session de tri sur 50 findings, l’investissement initial pour mettre en place les compteurs prend une journée.

Étape 1 — Trier les findings au lieu de les éliminer aveuglément

Le premier réflexe d’une équipe submergée est de désactiver les règles bruyantes. C’est l’erreur fondatrice : derrière une règle qui produit beaucoup de bruit se cachent souvent quelques vrais findings critiques noyés dans la masse. La discipline commence par trier finding par finding, en attribuant chacune à l’une de quatre catégories.

Catégorie Action
True positive exploitable Créer un ticket, assigner à une squad, tag security-debt
True positive non exploitable (vulnérabilité présente mais surface bloquée par un middleware ou une auth) Documenter la mitigation existante, marquer not-exploitable avec lien vers le code de mitigation
False positive — règle trop large Affiner la règle (étape 3), ne pas suppress un par un
False positive — cas légitime spécifique Suppression inline avec justification (étape 4)

Cette grille évite l’écueil de tout étiqueter false positive. La distinction entre true positive non exploitable et false positive est particulièrement importante : la première reste un risque latent à surveiller, la seconde n’a jamais été un risque. Mélanger les deux conduit à perdre la trace des mitigations et à accumuler de la dette implicite.

Étape 2 — Mesurer le taux de faux positifs par règle

Sans mesure, impossible de cibler les règles à travailler. La métrique pivot est le ratio FP par règle : pour chaque rule-id, combien de findings ont été marqués false positive sur le total émis. DefectDojo expose ce calcul nativement via son API, mais on peut le reproduire avec un script Python en quelques lignes.

import os
import requests
from collections import defaultdict

DOJO = "https://defectdojo.example.com"
TOKEN = os.environ["DOJO_API_KEY"]
HEADERS = {"Authorization": f"Token {TOKEN}"}

stats = defaultdict(lambda: {"total": 0, "fp": 0})
url = f"{DOJO}/api/v2/findings/?limit=1000&active=true"
while url:
    r = requests.get(url, headers=HEADERS).json()
    for f in r["results"]:
        rule = f.get("vuln_id_from_tool", "unknown")
        stats[rule]["total"] += 1
        if f.get("false_p"):
            stats[rule]["fp"] += 1
    url = r["next"]

ranked = sorted(
    ((r, s["fp"] / s["total"], s["total"]) for r, s in stats.items() if s["total"] >= 5),
    key=lambda x: x[1], reverse=True
)
for rule, ratio, total in ranked[:20]:
    print(f"{ratio:5.1%}  {total:4d}  {rule}")

La sortie classe les 20 règles les plus bruyantes. Une règle au-dessus de 70 % de FP avec 30+ occurrences est candidate prioritaire à la révision : soit la règle elle-même est à raffiner, soit le code de l’équipe contient un pattern légitime que la règle ne sait pas reconnaître. Le seuil total >= 5 évite de classer en tête une règle qui a été déclenchée deux fois et marquée FP par hasard.

Étape 3 — Raffiner une règle Semgrep trop large

Imaginons que la règle python.lang.security.audit.dangerous-system-call matche tous les os.system(...) et génère 80 % de FP parce que votre équipe utilise os.system() pour des commandes hard-codées dans des scripts d’admin. La solution n’est pas de désactiver la règle mais d’ajouter un pattern-not qui exclut les cas légitimes.

rules:
  - id: dangerous-system-call-refined
    languages: [python]
    severity: HIGH
    message: |
      os.system() avec une chaîne dynamique permet une command injection.
      Utiliser subprocess.run([...]) avec une liste d'arguments.
    pattern: os.system($CMD)
    pattern-not:
      - pattern: os.system("...")
      - pattern: os.system(f"...")
      - pattern: os.system(CONST_CMD)
    metavariable-regex:
      metavariable: $CMD
      regex: ^[a-zA-Z_]\w*$|.*\+.*|.*\.format\(.*

L’idée centrale : le risque vient de la concaténation et du formatage de chaîne, pas des littéraux ou des constantes globales. Le pattern-not exclut les chaînes littérales (toujours sûres puisque hard-codées) et les os.system(CONST_CMD)CONST_CMD est une constante. Le metavariable-regex resserre encore en exigeant que la métavariable $CMD soit suspecte — concaténation ou formatage. Sur le terrain, ce raffinage ramène le taux de FP de 80 % à moins de 10 %.

Étape 4 — Suppressions inline avec justification et expiration

Pour les cas où la règle est juste mais le contexte spécifique justifie l’exception, l’annotation inline est la voie propre. Trois règles cardinales : la justification est obligatoire, la date d’expiration est explicite, le commentaire est revu en code review.

# nosemgrep: dangerous-system-call - Le chemin est validé par validate_safe_path() ligne 42.
# Expire 2026-12-31 - revoir la nécessité de subprocess vs os.system.
os.system(f"backup --target={validate_safe_path(target)}")

L’annotation Semgrep nosemgrep: sur la ligne précédant l’occurrence désactive la règle pour cette ligne uniquement. La justification est en clair, lisible. La date d’expiration est moralement contraignante : un script de revue trimestrielle peut grep nosemgrep: et signaler les expirations dépassées.

grep -rn 'Expire \(2026\)' --include='*.py' . | \
  awk -F'Expire ' '{print $2}' | \
  awk -F' ' '{if ($1 < "'"$(date +%F)"'") print}'

Ce one-liner liste les annotations dont la date d’expiration est passée. À intégrer dans un job CI hebdomadaire qui ouvre une issue automatique si une expiration n’a pas été traitée. Sans ce mécanisme, les annotations s’accumulent et les exceptions deviennent permanentes par défaut.

Étape 5 — SonarQube Issue Status et tag suppression

SonarQube expose un workflow d’issue intégré qui distingue cinq statuts : Open, Confirmed, Resolved (avec sous-statut Fixed, False Positive, Won’t Fix), Reopened, Closed. Le statut False Positive retire l’issue du Quality Gate sans la supprimer définitivement. Le passage au statut nécessite un commentaire ; SonarQube refuse de marquer une issue FP sans justification.

Au-delà du statut, les tags servent à documenter la nature de l’exception. Convention recommandée pour l’équipe :

Tag Usage
fp-pattern-trop-large FP dû à une règle imprécise — à remonter dans Sonar Issue Tracker
fp-context-specifique FP justifié par un contexte que la règle ne peut pas voir
not-exploitable-mitigated True positive avec mitigation effective ailleurs dans le code
accepted-risk-2026 Risque accepté par décision RSSI, expire à la fin de l’année

L’export hebdomadaire des issues taggées accepted-risk-* via GET /api/issues/search?tags=accepted-risk-2026 alimente un comité de revue trimestriel avec le RSSI. Cette traçabilité fait la différence entre une équipe sécurité crédible et une équipe qui accumule des risques sous le tapis.

Étape 6 — Trivy avec ignorefile structuré et VEX

Trivy lit un fichier .trivyignore à la racine du dépôt ou via --ignorefile. Le format simple ignore une CVE par ligne, mais Trivy 0.50+ accepte aussi le format VEX (Vulnerability Exploitability eXchange) qui structure la justification au standard CycloneDX 1.7.

# trivy-vex.yaml
metadata:
  component:
    bom-ref: "pkg:image/myapp@sha256:abc123"
    type: "container"
vulnerabilities:
  - id: CVE-2024-12345
    analysis:
      state: "not_affected"
      justification: "vulnerable_code_not_in_execute_path"
      detail: "La fonction vulnérable openssl_sign_old() n'est pas invoquée par notre code."
      response: ["will_not_fix"]
    affects:
      - ref: "pkg:image/myapp@sha256:abc123"

Cinq justifications sont normalisées : code_not_present, code_not_reachable, requires_configuration, requires_dependency, requires_environment, protected_by_compiler, protected_at_runtime, protected_at_perimeter, protected_by_mitigating_control. Choisir la plus précise possible : un auditeur ou un futur mainteneur comprendra immédiatement pourquoi la CVE a été acceptée. Pour appliquer le VEX au scan : trivy image --vex trivy-vex.yaml myimage.

Étape 7 — Boucle de feedback avec les développeurs

Une règle bruyante l’est rarement par hasard : elle correspond à une situation où le code de l’équipe diverge des hypothèses standard de l’outil. Les développeurs les plus exposés au bruit sont les meilleurs sources d’amélioration. Trois mécanismes structurent cette boucle.

Le premier est le canal Slack ou MM dédié où les développeurs signalent les FP en deux clics : un bouton dans l’issue tracker SonarQube, un menu contextuel dans VS Code via l’extension Semgrep, un workflow GitHub Actions qui crée une issue de tracking. Le second est la revue mensuelle des règles bruyantes, pendant laquelle l’équipe sécurité présente le top 10 des règles à plus haut FP et discute du raffinage avec deux ou trois développeurs représentatifs des squads concernées. Le troisième est la responsabilisation écrite : chaque règle a un mainteneur identifié dans le repo de règles, qui répond aux issues sous 48 h.

Étape 8 — Rejouer la baseline régulièrement

Une suppression posée il y a 18 mois peut avoir perdu sa pertinence : la fonction de mitigation a été supprimée, la CVE marquée not-affected est devenue exploitable par un nouveau chemin de code, le développeur qui a justifié l’exception a quitté l’équipe. Le rejeu trimestriel de la baseline est une pratique salutaire.

#!/usr/bin/env bash
# baseline-review.sh - à lancer trimestriellement
set -euo pipefail

# 1. Lister les nosemgrep et leurs lignes
grep -rn 'nosemgrep:' --include='*.py' --include='*.js' --include='*.go' . > current-suppressions.txt

# 2. Lister celles dont la date d'expiration est passée
awk '/Expire / { match($0, /Expire ([0-9]{4}-[0-9]{2}-[0-9]{2})/, a); if (a[1] < "'"$(date +%F)"'") print }' current-suppressions.txt

# 3. Comparer aux suppressions de la version précédente
git show HEAD~90:suppressions.txt 2>/dev/null | comm -23 - current-suppressions.txt

Le rapport trimestriel récapitule trois informations : combien d’annotations ont expiré et n’ont pas été renouvelées, combien de nouvelles annotations sont apparues sans revue, et le ratio global suppressions/findings actifs. Ces trois chiffres sont l’indicateur de santé du programme. Une augmentation rapide des suppressions sans baisse correspondante du nombre de FP signale une dérive : l’équipe escamote au lieu de corriger.

Erreurs fréquentes

Symptôme Cause Solution
L’équipe désactive les règles bruyantes en bloc Pas de processus de tri par finding Imposer la grille des 4 catégories dès la première semaine, mesurer le ratio FP par règle
Le fichier .trivyignore contient 50 entrées sans justification Pas de discipline d’écriture initiale Reformer un fichier propre depuis zéro, refuser les commits qui ajoutent une ligne sans commentaire
Les annotations nosemgrep n’ont jamais d’expiration Absence de modèle imposé en code review Mettre en place un linter qui rejette les annotations sans date
Les développeurs râlent que SonarQube affiche encore des FP fermés Confusion entre filtre Open/All Former à filtrer sur status=Open dans la vue par défaut
Le ratio FP global ne descend pas malgré l’investissement Pas de boucle de feedback sur les règles Revue mensuelle structurée des 10 règles les plus bruyantes
VEX mal interprété par les outils en aval Justification non standard Utiliser exclusivement les valeurs normalisées CycloneDX 1.7

FAQ

Faut-il viser zéro faux positif ? Non, c’est irréaliste avec n’importe quel outil statique. Un programme mature vit avec 5 à 15 % de FP, sous deux conditions : ils sont systématiquement triés et tracés, et le ratio reste stable ou décroissant. Au-dessus de 30 %, l’outil perd sa valeur opérationnelle.

Quelle différence entre not-exploitable et false positive en VEX ? Un FP est une erreur de l’outil — la vulnérabilité n’a jamais existé. Un not-exploitable reconnaît la présence du code vulnérable mais affirme que la surface est protégée par un facteur externe (auth, configuration, mitigation runtime). Le second mérite une revue trimestrielle ; le premier peut être oublié sereinement.

Comment éviter qu’un développeur abuse des annotations nosemgrep ? Code review obligatoire avec règle d’or : toute annotation doit être validée par un security champion ou un reviewer senior. Métriques sur le nombre d’annotations par contributeur — un dev avec 50 annotations en six mois mérite une discussion.

Le tri des findings doit-il être centralisé ou réparti ? Centralisé pour la cohérence des règles et la mesure du ratio FP, réparti par squad pour le triage opérationnel. Le pattern qui marche : un security champion par squad fait le premier tri, puis remonte les règles à raffiner à l’équipe sécurité centrale.

Combien de temps allouer au tri par sprint ? Pour une squad de 5 à 8 développeurs, deux à quatre heures par sprint suffisent en régime stable. La première année demande 2 à 3 fois plus pour traiter la dette historique.

Tutoriels associés

Lectures recommandées

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité