Le Pipeline Ultime dans GitLab : Qualité, Tests et DevSecOps
Le déploiement d'une application ne doit plus ressembler à un saut dans le vide. Dans un modèle de développement traditionnel, la qualité du code, les tests et la sécurité étaient gérés en silos, souvent à la toute fin du projet. Le résultat ? Des retards massifs, du stress et des refontes coûteuses.
Aujourd'hui, l'approche CI/CD moderne renverse cette logique. Il s'agit du principe de "Shift-Left" : ramener toutes ces vérifications le plus tôt possible dans la chronologie du projet. Dans cet article, nous allons construire un pipeline GitLab complet, une véritable douane automatisée qui va auditer, tester et sécuriser votre travail à chaque ligne de code poussée.
"Un pipeline blindé : Lint -> Test -> Build -> SAST -> DAST"
Assurer la Qualité du Code (Linting)
Le Linting est la première ligne de défense de votre pipeline. Un "Linter" est un outil d'analyse statique qui vérifie si votre code respecte les standards de l'industrie. Contrairement à un test, il ne vérifie pas si le code marche, mais s'il est bien écrit.
Pourquoi est-ce indispensable ?
- Maintenabilité : Tout le code de l'équipe suit la même structure (indentation, nommage), facilitant la relecture.
- Détection d'erreurs bêtes : Il repère les variables déclarées mais jamais utilisées, les parenthèses manquantes ou les imports inutiles.
- Économie de ressources : Un job de linting prend quelques secondes. Il vaut mieux qu'un pipeline échoue ici plutôt qu'après 10 minutes de build Docker pour une simple virgule oubliée.
Exemple concret : Linting Python (Flake8)
lint_python:
stage: test
image: python:3.9-slim
script:
- pip install flake8
- flake8 --exclude=.venv,migrations --max-line-length=120 .
Dans cet exemple, notre code va vérifier automatiquement si vous avez respecté les standards de la communauté Python (PEP). Le linter va traquer les indentations incorrectes, les variables déclarées mais jamais utilisées, ou encore les lignes trop longues qui rendent la relecture difficile. Si une seule règle n'est pas respectée, le pipeline s'arrête immédiatement, vous forçant à livrer un code propre.
Exemple concret : Linting JavaScript (ESLint)
lint_javascript:
stage: test
image: node:latest
script:
- npm install
- npm run lint
Ici, le job utilise eslint pour passer votre projet JavaScript au peigne fin. Notre code va vérifier la présence de points-virgules manquants, l'utilisation de méthodes obsolètes ou des erreurs de logique potentielles avant même que l'application ne soit lancée. C'est un gain de temps énorme : le robot détecte en une seconde ce qu'un humain pourrait rater lors d'une relecture manuelle.
Les Tests Automatisés
Une fois que le code est propre, il faut s'assurer qu'il remplit sa mission. Les tests automatisés empêchent les régressions (le fait de casser une fonctionnalité existante en en ajoutant une nouvelle).
Les Tests Unitaires (Unit Tests)
Ils testent une fonction ou un composant de manière isolée. C'est le test le plus rapide et le plus précis.
unit_tests:
stage: test
image: python:3.9
script:
- pip install pytest
- pytest tests/unit/ --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
Dans ce job, notre code va vérifier la logique pure de vos fonctions sans aucune dépendance externe. L'option --junitxml=report.xml est cruciale : elle génère un fichier de rapport structuré. Grâce à la section artifacts:reports:junit, GitLab va lire ce fichier et transformer vos résultats de tests en une interface claire dans vos Merge Requests. Vous verrez directement quelle fonction a échoué sans jamais ouvrir les logs du terminal.
Les Tests d'Intégration (Services)
Ces tests vérifient la communication entre votre code et ses dépendances (base de données, cache). GitLab CI permet de monter des Services (des conteneurs éphémères) pour ces tests.
Exemple : Tester une connexion à une base PostgreSQL
integration_tests:
stage: test
image: python:3.9
services:
- postgres:13
variables:
POSTGRES_DB: test_db
POSTGRES_USER: user
POSTGRES_PASSWORD: password
DATABASE_URL: "postgresql://user:password@postgres:5432/test_db"
script:
- pip install -r requirements.txt
- pytest tests/integration/
Ici, la puissance de GitLab réside dans le mot-clé services. Notre code va vérifier si votre application est capable de lire et d'écrire dans une véritable base de données PostgreSQL. GitLab démarre un conteneur Postgres isolé uniquement pour la durée du test. La variable DATABASE_URL utilise le nom de service postgres comme hôte, car GitLab lie automatiquement les deux conteneurs par le réseau. C'est l'assurance que votre code fonctionnera une fois relié à une vraie base en production.
Couverture de code (Code Coverage)
La couverture de code indique le pourcentage de votre code qui est réellement testé. C'est un indicateur de confiance majeur.
test_coverage:
stage: test
script:
- pytest --cov=my_app tests/
coverage: '/TOTAL.*\s+(\d+%) /'
Le mot-clé coverage utilise une expression régulière (Regex) pour scanner la sortie texte de vos tests. Notre code va vérifier les logs à la recherche d'un motif spécifique (ici le total en pourcentage). Une fois extrait, GitLab affichera ce score de couverture directement sur le badge du projet et dans la Merge Request. Cela permet de voir immédiatement si un développeur a ajouté du code sans écrire les tests correspondants.
"Badge Code Coverage Gitlab"
Déployer les Boucliers de Sécurité (DevSecOps)
La puissance des "Templates" GitLab
L'immense avantage de GitLab est que vous n'avez pas besoin de scripter ces outils de sécurité vous-même. GitLab maintient des templates officiels (des bouts de pipeline pré-écrits) qu'il suffit d'inclure (include) dans votre fichier .gitlab-ci.yml.
Secret Detection
C'est l'erreur bête mais fatale : un développeur copie-colle un code de test et oublie de retirer le mot de passe de la base de données ou le token de l'API AWS avant de faire son commit.
Ce que ça fait ? Le scanner de Secret Detection lit vos fichiers textes à la recherche de formats connus (clés privées RSA, tokens AWS, mots de passe). S'il en trouve un, il fait échouer le pipeline.
L'intégration dans votre pipeline :
include:
- template: Security/Secret-Detection.gitlab-ci.yml
SAST (Static Application Security Testing)
Le SAST est un scanner "statique". Il analyse votre code source comme s'il lisait un livre, sans jamais avoir besoin de lancer l'application. Il détecte les mauvaises pratiques de codage.
- Exemple d'attaque bloquée : Une Injection SQL en Python ou en PHP, ou l'utilisation d'une librairie de cryptographie obsolète (comme MD5).
- L'avantage : Il détecte tout seul le langage que vous utilisez (Python, Java, JavaScript, etc.) et lance l'outil d'analyse approprié.
L'intégration dans votre pipeline :
include:
- template: Security/SAST.gitlab-ci.yml
Container Scanning
Même si votre code PHP ou Node.js est parfait, vous allez probablement l'empaqueter dans une image Docker (avec Alpine, Ubuntu, etc.). Mais que se passe-t-il si la version d'Alpine que vous utilisez possède une faille de sécurité système (une CVE) publique ?
Ce que ça fait ? Dès que votre image Docker est construite, le Container Scanning l'ouvre et vérifie tous les paquets Linux installés à l'intérieur contre une base de données mondiale de failles connues.
L'intégration dans votre pipeline :
include:
- template: Security/Container-Scanning.gitlab-ci.yml
DAST (Dynamic Application Security Testing)
Le DAST est un scanner "dynamique". Contrairement au SAST, il ne lit pas le code source. Il a besoin que votre application soit déployée et accessible sur une vraie URL (par exemple, un environnement de Staging ou de test).
Ce que ça fait ? Il agit comme un vrai hacker de l'extérieur. Il va cliquer sur vos boutons, remplir vos formulaires avec du code malveillant pour tenter de déclencher des failles comme le XSS (Cross-Site Scripting) ou forcer les URL.
Le DAST ne devine pas tout par magie. Il utilise deux méthodes principales pour comprendre votre application : le Crawling et l'Authentification.
L'exploration automatique (Le Crawler)
L'intégration requiert de cibler une URL :
include:
- template: Security/DAST.gitlab-ci.yml
variables:
DAST_WEBSITE: "https://mon-application-de-test.com"
Dès que vous lui donnez une URL via la variable DAST_WEBSITE, le scanner lance un "Crawler" (similaire à celui de Google).
- Découverte des liens : Il scanne le code HTML de la page d'accueil pour trouver tous les liens (balises <a href="...">) et les suit un par un.
- Analyse des formulaires : Il repère tous les champs de saisie (input, formulaires de contact, barres de recherche).
- Le "Fuzzing" : Une fois qu'il a listé ces points d'entrée, il injecte des milliers de payloads malveillants (ex: du script pour le XSS) dans chaque champ pour voir comment le serveur réagit.
Lui donner le contexte (L'Authentification)
Si votre site possède une page de connexion, le crawler va se retrouver bloqué devant le mur du "Login". Pour qu'il puisse tester l'intérieur de l'application, vous devez lui donner les clés.
Exemple de configuration dans le .gitlab-ci.yml :
variables:
DAST_WEBSITE: "https://mon-app-test.com"
DAST_AUTH_URL: "https://mon-app-test.com/login"
DAST_USERNAME: "robot_scanner"
DAST_PASSWORD: $ROBOT_PASSWORD
DAST_USERNAME_FIELD: "user_email"
DAST_PASSWORD_FIELD: "user_password"
Utiliser un plan de route (OpenAPI / Swagger)
Si vous testez une API (sans interface visuelle), le crawler ne sert à rien car il n'y a pas de boutons. Dans ce cas, vous fournissez au DAST un fichier OpenAPI (Swagger).
Exemple de configuration (.gitlab-ci.yml) :
include:
- template: Security/DAST-API.gitlab-ci.yml
variables:
DAST_API_TARGET_URL: "https://api-staging.mon-entreprise.com"
DAST_API_OPENAPI: "https://api-staging.mon-entreprise.com/docs/swagger.json"
- Le fichier swagger.json est le "plan de la ville". Il liste toutes les routes (ex: "/api/users", "/api/login") et les types de données attendus.
- Le DAST lit ce plan et attaque systématiquement chaque route listée.
Exemple de structure type d'un fichier Swagger (JSON) :
{
"openapi": "3.0.0",
"info": {
"title": "Mon API de Staging",
"version": "1.0.0",
"description": "API de test pour le scan DAST"
},
"paths": {
"/users": {
"get": {
"summary": "Récupère la liste des utilisateurs",
"responses": {
"200": { "description": "Succès" }
}
},
"post": {
"summary": "Crée un utilisateur",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": { "type": "string", "example": "Jean Dupont" },
"email": { "type": "string", "example": "jean@exemple.com" }
}
}
}
}
},
"responses": {
"201": { "description": "Utilisateur créé" }
}
}
},
"/users/{id}": {
"delete": {
"summary": "Supprime un utilisateur",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "integer" }
}
],
"responses": {
"204": { "description": "Supprimé" }
}
}
}
}
}
Comment le DAST exploite ce fichier ?
- Identification des cibles : Le DAST voit qu'il y a trois opérations possibles : GET /users, POST /users et DELETE /users/{id}.
- Test d'injection : Pour le POST, il sait qu'il doit envoyer du JSON. Il va remplacer "Jean Dupont" par des scripts comme <script>alert(1)</script> pour tester les failles XSS.
- Test de paramètres : Pour le DELETE, il va essayer de remplacer l'ID par des commandes SQL (ex: 1 OR 1=1) pour tenter une injection SQL.
- Vérification des réponses : Si le serveur répond par une erreur 500 détaillée au lieu d'une erreur 400 propre, le DAST signalera une vulnérabilité de fuite d'informations.
Est-ce qu'il peut tout casser ?
Oui. Le DAST va essayer de supprimer, modifier et créer des données. C'est pour cela qu'on ne lance JAMAIS un DAST sur la production. On l'utilise uniquement sur un environnement de Staging ou de Review App contenant des données de test.
Conclusion
En empilant ces trois couches de vérification, le Linting pour la forme, les Tests pour le fond, et les Scanners (SAST/DAST) pour la sécurité, votre pipeline GitLab se transforme en un véritable garde du corps pour votre application.
Vous n'avez plus besoin d'être un expert en cybersécurité ou de relire chaque ligne de code manuellement pour éviter les catastrophes. Grâce à l'automatisation, toute erreur de syntaxe, bug de régression ou faille critique est intercepté avant la fusion. Cette stratégie globale permet à votre équipe de se concentrer sur l'essentiel : créer de la valeur, tout en garantissant des déploiements en production sereins et sécurisés.
Espace commentaire
Écrire un commentaire
Rejoignez la discussion
Vous devez être connecté pour poster un message.
25 commentaires
Vérifie que ton fichier swagger est bien accessible publiquement par le runner ou qu'il est servi par ton app au bon endpoint.
Question bête : le
Secret Detectionscanne aussi les commits passés ou juste le dernier ?Il scanne tout l'historique sur la branche courante. Si tu as leaké un token il y a 3 mois, il va le choper. Bon courage pour le nettoyage.
J'ai un souci avec mon
unit_tests. Le fichierreport.xmlest bien généré mais GitLab ne l'affiche pas dans la MR.Vérifie bien l'indentation sous
artifacts. Ça doit être exactement comme ça :Je galère avec le
servicespostgres. Il refuse de se connecter au port 5432. Une idée ?Le conteneur Postgres met parfois du temps à démarrer. Essaie d'ajouter un script
sleep 5dans tonscriptavant de lancer les tests pour laisser le temps au service d'être prêt.Pour la couverture de code, mon regex ne matche rien, le score reste à 0%. Quelqu'un a un exemple pour du Go ?
Pour Go, utilise ceci :
coverage: '/coverage: \d+.\d+% of statements/'. Adapte le selon la sortie dego test -cover.Le DAST tente de supprimer mes données de test. Comment empêcher ça ?
Si ton API a une route
DELETE, le scanner va la tester. Soit tu isoles tes routes de test, soit tu configures le DAST pour exclure certaines URLs avecDAST_EXCLUDE_URLS.Merci pour l'astuce sur le
Secret Detection, ça m'a sauvé d'un commit malheureux.Pense à installer
git-secretsen local sur tes machines dev, ça évite même d'arriver au stage de CI.Super article. Par contre, le
DAST-APIéchoue chez moi. Il ne trouve pas le fichierswagger.json.Surtout pas. Si ton image est vulnérable, ton app l'est aussi. Mets à jour ton
Dockerfilepour utiliser une versionslimoualpineplus récente.Le scan de conteneur me remonte des failles critiques sur une image Debian obsolète. Je peux ignorer ces alertes ?
Ajoute une étape de cache pour tes dépendances, sinon le runner réinstalle tout à chaque fois. Exemple :
J'ai essayé d'implémenter le linting Python avec
flake8comme montré dans l'article, mais ça me dit que le module est introuvable.Tu dois utiliser les variables masquées dans les paramètres du projet GitLab. Va dans Settings > CI/CD > Variables et coche
Masked.Comment vous gérez le
DAST_PASSWORD? Je ne veux pas le mettre en dur dans le yaml.Le DAST fait du fuzzing intensif, c'est normal. Si tu veux accélérer, utilise
DAST_FULL_SCAN_ENABLED: "false"pour limiter le scope, mais c'est à ne jamais faire en prod.Merci, c'était bien ça. Par contre, le DAST est super lent sur mon infra. Il met 20 minutes à scanner 3 routes.
Vérifiez que votre runner utilise bien l'exécuteur
dockeret nonshelldans la configconfig.toml. Le scan a besoin d'isoler le conteneur.Idem ici. J'ai ajouté le template
Security/SAST.gitlab-ci.ymlmais ça me sort une erreurdocker: command not found.J'ai testé ton setup SAST, mais mon pipeline plante dès le début avec un souci de permissions sur le runner. Quelqu'un a eu le même problème ?