ITSkillsCenter
Business Digital

Bloquer les PR sur les nouvelles vulnérabilités sans freiner l’équipe

13 دقائق للقراءة

📍 Guide principal du sujet : Pipeline SAST DAST SCA 2026 : architecture, outils et intégration CI/CD
Ce tutoriel détaille la stratégie qui rend un pipeline DevSecOps acceptable pour les développeurs : ne bloquer que les nouvelles vulnérabilités, jamais la dette historique.

Pourquoi adopter la politique fail-on-new-issue plutôt que fail-on-any

Quand une équipe active SAST, SCA et DAST simultanément sur un dépôt vivant depuis cinq ans, le premier scan révèle généralement des centaines de findings hérités. Si la politique CI bloque toute pipeline présentant une vulnérabilité, plus aucun merge ne passe et l’équipe n’a que deux options : désactiver les scans (perte sèche) ou geler tout développement le temps de corriger la dette (impossible). La politique fail-on-new-issue résout ce dilemme : elle compare les findings de la pull request à un état de référence (la baseline), et n’échoue le job qu’en cas d’apparition d’un finding qui n’existait pas auparavant.

Cette stratégie produit un effet de cliquet : la dette historique reste visible dans les rapports mais n’empêche personne d’avancer, tandis qu’aucun nouveau problème ne peut s’introduire silencieusement. À mesure que l’équipe traite la dette, la baseline rétrécit naturellement. Le concept est implémenté différemment selon les outils — --baseline-commit dans Semgrep, New Code Period dans SonarQube, Vulnerability State dans GitLab Security Policies — mais le principe reste identique. Ce tutoriel passe en revue chaque implémentation et donne le squelette de pipeline GitLab CI et GitHub Actions correspondant.

Prérequis

  • Un dépôt Git existant avec un pipeline CI fonctionnel.
  • Au moins un outil SAST ou SCA déjà branché (Semgrep, SonarQube, Trivy, ou les scanners natifs GitLab/GitHub).
  • Une compréhension du modèle pull request / merge request de votre forge.
  • Niveau attendu : intermédiaire DevOps, à l’aise avec les variables CI et la lecture d’un YAML de pipeline.
  • Temps estimé : 60 minutes pour mettre en place la politique sur un outil, 90 minutes pour la généraliser sur trois outils.

Étape 1 — Cartographier la dette existante

Avant de définir un seuil, il faut mesurer ce qui existe. Lancer chaque outil en mode informatif sur la branche principale et exporter le rapport. Sur Semgrep, par exemple, depuis la racine du dépôt :

semgrep scan --config auto --json --output baseline.json
jq '.results | group_by(.extra.severity) | map({severity: .[0].extra.severity, count: length})' baseline.json

La sortie résume le nombre de findings par sévérité. Sur un projet typique de 100 000 lignes Python actif depuis trois ans, vous trouverez généralement 5 à 30 findings HIGH/CRITICAL et plusieurs centaines de MEDIUM/LOW. Cette photo est précieuse à la fois comme baseline technique et comme indicateur de communication interne — annoncer « nous avons 12 findings critiques à traiter » est plus actionnable que « nous avons des problèmes de sécurité ».

Étape 2 — Activer le mode différentiel sur Semgrep

Semgrep supporte nativement la comparaison à un commit de référence avec l’option --baseline-commit. Le moteur scanne deux fois (l’état avant et l’état après), puis ne signale que les findings absents à l’état avant. Cette logique est fiable même quand un fichier change : un finding sur une ligne qui n’existait pas devient un nouveau finding, un finding sur une ligne déplacée reste considéré comme historique.

semgrep_diff:
  image: returntocorp/semgrep:1.161.0
  stage: scan
  script:
    - git fetch origin "$CI_DEFAULT_BRANCH"
    - semgrep scan
        --config auto
        --baseline-commit "origin/$CI_DEFAULT_BRANCH"
        --error
        --severity ERROR
        --severity WARNING
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Trois options méritent attention. --error renvoie un code de sortie non-nul dès qu’un nouveau finding apparaît, ce qui fait échouer le job CI. La double --severity limite le bloquant aux niveaux ERROR et WARNING (équivalent CRITICAL et HIGH dans la nouvelle taxonomie), laissant les INFO en informatif. La règle rules: - if: ... merge_request_event circonscrit le job aux MR uniquement, évitant les faux blocages sur les pushs directs main qui n’ont pas de baseline meaningful.

Étape 3 — Configurer SonarQube New Code Period

SonarQube ne raisonne pas par commit mais par New Code Period, une fenêtre temporelle ou versionnelle qui définit ce qui est « nouveau ». Trois modes existent : depuis une date précise, depuis une version (numéro de release), ou depuis la branche de référence. Ce dernier mode est le plus naturel pour une équipe qui pratique le trunk-based development.

Configurer dans Project Settings → New Code → Specific setting for this project et choisir Reference branch: main. À partir de cet instant, le Quality Gate ne tient compte que du code modifié depuis la dernière divergence avec main. Le badge Quality Gate Passed sur la PR reflète l’état du nouveau code, pas l’état global du projet.

sonar-scanner \
  -Dsonar.projectKey=mon-projet \
  -Dsonar.host.url=https://sonar.example.com \
  -Dsonar.login=$SONAR_TOKEN \
  -Dsonar.pullrequest.key=$CI_MERGE_REQUEST_IID \
  -Dsonar.pullrequest.branch=$CI_COMMIT_REF_NAME \
  -Dsonar.pullrequest.base=$CI_DEFAULT_BRANCH \
  -Dsonar.qualitygate.wait=true \
  -Dsonar.qualitygate.timeout=300

L’option sonar.qualitygate.wait=true bloque la sortie du scanner jusqu’au verdict du Quality Gate. Sans cette option, le scanner termine immédiatement et le job CI passe au vert avant même que SonarQube ait fini d’analyser. Le timeout de 300 secondes protège contre une queue saturée. Si le gate échoue, le scanner retourne 1 et le job CI échoue ; les développeurs reçoivent un commentaire automatique sur la MR avec le détail des conditions non satisfaites.

Étape 4 — Définir un Quality Gate strict pour le nouveau code

Le Quality Gate par défaut Sonar way est volontairement permissif. Pour une politique fail-on-new-issue stricte, créer un gate custom dans Quality Gates → Create avec les conditions suivantes appliquées au new code uniquement :

Condition Opérateur Seuil
New Vulnerabilities est plus grand que 0
New Security Hotspots Reviewed est inférieur à 100 %
New Bugs (sévérité CRITICAL) est plus grand que 0
New Coverage est inférieur à 80 %
New Duplicated Lines (%) est plus grand que 3

L’asymétrie est essentielle : zéro tolérance sur les nouvelles vulnérabilités, exigence forte sur la couverture du nouveau code (80 %), mais aucune contrainte sur le code existant. Cette configuration est suffisamment exigeante pour tenir l’équipe sous pression positive sans bloquer en boucle. Affecter le gate au projet via Project Settings → Quality Gate.

Étape 5 — Politiques GitLab Security et seuils par sévérité

Sur GitLab Premium, les Security Policies offrent une approche plus déclarative que les variables d’environnement par job. Une politique YAML s’applique à plusieurs projets via Compliance Frameworks et reste indépendante des fichiers .gitlab-ci.yml de chaque dépôt. Créer dans Security & Compliance → Policies → Scan result policy :

type: scan_result_policy
name: Fail on new high SAST vulnerabilities
description: Block MR with newly detected HIGH or CRITICAL SAST findings
enabled: true
rules:
  - type: scan_finding
    branches: [main, develop]
    scanners: [sast, secret_detection, container_scanning, dependency_scanning]
    vulnerabilities_allowed: 0
    severity_levels: [critical, high]
    vulnerability_states: [newly_detected]
actions:
  - type: require_approval
    approvals_required: 1
    role_approvers: [security_engineer]

Le couple vulnerabilities_allowed: 0 + vulnerability_states: [newly_detected] implémente exactement la sémantique fail-on-new : zéro nouveau finding HIGH ou CRITICAL toléré sans approbation explicite d’un security engineer. Pour une politique purement bloquante (sans escape hatch d’approbation), changer l’action en require_approval avec un rôle inexistant — la MR ne pourra jamais être approuvée et donc jamais mergée tant que le finding subsiste.

Étape 6 — Trivy avec ignorefile et seuils de sévérité

Trivy ne dispose pas d’option --baseline-commit directe, mais expose un .trivyignore qui sert de baseline manuelle des CVE acceptées. Le pattern recommandé combine seuil de sévérité minimum et ignorefile versionné dans le repo.

trivy image \
  --severity HIGH,CRITICAL \
  --exit-code 1 \
  --ignore-unfixed \
  --ignorefile .trivyignore \
  --format json \
  --output trivy-report.json \
  $IMAGE_TAG

L’option --exit-code 1 fait échouer le job dès qu’une CVE non ignorée et de sévérité HIGH ou CRITICAL apparaît. --ignore-unfixed exclut les CVE pour lesquelles aucun patch n’existe encore, évitant le blocage sur des situations où le développeur ne peut rien faire. Le fichier .trivyignore contient une CVE par ligne avec un commentaire et idéalement une date d’expiration :

# CVE-2024-12345 - false positive sur lib X, ne s'applique pas au mode utilisé. Réviser 2026-09-01.
CVE-2024-12345
# CVE-2024-67890 - exploitable uniquement en cas d'accès admin local. Acceptée par RSSI 2026-04-15.
CVE-2024-67890

La revue trimestrielle de l’ignorefile fait partie du rituel sécurité : sans date d’expiration, un fichier qui démarre à 5 lignes finit à 200 en deux ans, vidant la politique de son sens.

Étape 7 — Implémenter dans GitHub Actions avec Code Scanning

GitHub Code Scanning calcule automatiquement le différentiel entre l’état de la branche cible et celui de la PR. La politique fail-on-new se configure au niveau du dépôt via Settings → Code security → Code scanning → Default branch, en choisissant le seuil de sévérité qui doit échouer le check.

name: Code scanning
on:
  pull_request:
    branches: [main]
permissions:
  security-events: write
  contents: read
jobs:
  analyze:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v4
        with:
          languages: javascript-typescript, python
      - uses: github/codeql-action/analyze@v4

L’étape analyze publie automatiquement le SARIF dans GitHub Code Scanning ; la sévérité bloquante est ensuite définie au niveau du dépôt dans Settings → Code security → Code scanning → Protection rules, en choisissant le niveau (par exemple « Block PRs with errors of severity High and above »). Combinée avec une branch protection rule qui exige le check CodeQL green pour merger (configurée dans Settings → Rules → Rulesets → Require status checks to pass), cette politique bloque effectivement les PR fautives sans dépendre d’une action tierce.

Étape 8 — Mécanisme de dérogation tracée

Une politique stricte sans escape hatch est insoutenable. Quand un développeur a une raison légitime de merger malgré un finding (faux positif confirmé, contexte connu, dette acceptée temporairement), il doit pouvoir le faire — mais avec trace. Trois mécanismes complémentaires structurent l’évasion contrôlée.

Le premier est l’annotation inline : # nosemgrep: rule-id - justification en Semgrep, // trivy:ignore:CVE-... - raison en Trivy, ou // codeql[ql/...] = "raison" en CodeQL. La justification est obligatoire dans la revue de code. Le deuxième est l’approval surnuméraire dans les Security Policies GitLab : un security engineer doit approuver explicitement, son nom apparaît dans l’historique. Le troisième est le flag dismissed dans GitHub Code Scanning ou DefectDojo, avec choix obligatoire d’une raison parmi false positive, used in tests, won’t fix.

Une bonne politique combine les trois : annotation pour les cas clairs et locaux, approval pour les cas qui méritent un regard expert, dismiss pour la documentation centrale. La revue trimestrielle des dismissals est un moment salutaire pour repérer les abus et tuner les règles fautives.

Erreurs fréquentes

Symptôme Cause Solution
Tous les findings sont marqués comme « nouveaux » à chaque scan Pas de baseline configurée ou commit de baseline introuvable Vérifier git fetch origin main avant le scan, sinon Semgrep ne peut pas comparer
SonarQube Quality Gate Passed mais des findings critiques visibles Le gate ne contient que des conditions sur new code, la dette historique passe C’est le comportement voulu : créer un dashboard séparé pour suivre la dette globale
Trivy bloque sur une CVE pour laquelle aucun fix n’existe Pas de --ignore-unfixed Ajouter le flag, ou whitelister explicitement la CVE avec date d’expiration courte
Politique GitLab Approval ne se déclenche pas Politique limitée à main alors que les MR ciblent release/* Étendre branches: avec un wildcard ou ajouter explicitement les branches cibles
Le nombre de findings « new » oscille à chaque MR Différence entre le scan local du dev et le scan CI (versions différentes) Pinner la version de l’outil à l’identique en local et en CI
Annotation nosemgrep ignorée L’annotation est sur la mauvaise ligne Placer juste au-dessus de la ligne incriminée, sans ligne vide entre les deux

FAQ

Faut-il échouer sur les MEDIUM ou seulement sur les HIGH/CRITICAL ? Le compromis pratique est de bloquer sur HIGH/CRITICAL et de signaler informativement sur MEDIUM. Bloquer sur MEDIUM en démarrage génère trop de friction et incite l’équipe à désactiver l’outil. À mesure que le ratio de faux positifs descend sous 10 %, on peut resserrer.

Comment justifier la politique auprès des développeurs réticents ? Démontrer concrètement avec deux statistiques : (1) le nombre de minutes CI gagnées par MR (le scan différentiel est typiquement 5 à 10 fois plus rapide que le scan complet), et (2) le nombre de findings réellement corrigés en six mois grâce à la pression sur le nouveau code. Ces deux chiffres montrent que la politique sert l’équipe, pas une compliance abstraite.

Que faire d’un legacy qui dépasse 1 000 findings ? Refuser la tentation du grand nettoyage. Activer fail-on-new immédiatement pour stopper l’hémorragie, puis allouer un security debt budget de 10 à 20 % du sprint pour traiter la dette par sévérité décroissante. En 12 à 18 mois, la dette redescend sous le seuil acceptable sans avoir mobilisé personne en mode crise.

Le mode différentiel sait-il détecter une régression de sécurité corrigée puis réintroduite ? Oui dans la plupart des cas : la baseline étant la branche cible, si une CVE est corrigée sur main puis qu’une MR supprime le fix, le scan différentiel détecte la régression comme un nouveau finding. Cette propriété est précieuse pour éviter les régressions silencieuses lors de merges complexes.

Comment mesurer le succès de la politique ? Trois indicateurs : (1) le pourcentage de MR qui échouent au check sécurité (cible 5 à 15 %), (2) le délai médian entre détection et correction d’un finding HIGH (cible < 14 jours), (3) le nombre de dismissals justifiés sur le trimestre (à comparer à la baseline pour détecter une banalisation).

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é