En 2024, l'attaque xz-utils a démontré qu'un mainteneur malveillant pouvait injecter une backdoor dans un projet open source critique. La seule défense ? Une chaîne de build traçable et vérifiable où chaque étape est attestée.
Ce guide vous accompagne pas à pas pour construire un pipeline CI/CD hautement sécurisé pour un projet Python. À la fin, vous aurez :
- Un dépôt GitHub avec branch protection et Scorecard
- Des dépendances épinglées avec hash et auditées automatiquement
- Une image Docker avec attestation SLSA L3 et SBOM
- Des artefacts signés avec Sigstore (keyless)
- Des vérifications automatiques à chaque étape
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Configurer un dépôt GitHub durci : rulesets, signature des commits, Scorecard
- Épingler les dépendances Python par hash et les actions par SHA de commit
- Construire une image Docker multi-stage avec un utilisateur non-root
- Générer une attestation SLSA L3 et un SBOM à chaque release
- Signer l'image avec Cosign en mode keyless et vérifier sa provenance
- Centraliser la visibilité supply chain de tous vos services avec GUAC
Ce qu'on va construire
Section intitulée « Ce qu'on va construire »Une application Python minimaliste (API FastAPI) avec un pipeline qui :
- Analyse le code : linting, tests, SAST avec attestations in-toto
- Audite les dépendances : vulnérabilités, intégrité (hash pinning)
- Construit l'image Docker avec attestation SLSA L3
- Génère le SBOM (liste des composants) en SPDX et CycloneDX
- Signe l'image avec Cosign (keyless via Sigstore)
- Publie sur GHCR avec provenance vérifiable
- Ingère dans GUAC pour visibilité globale sur les dépendances
- Évalue avec Scorecard les bonnes pratiques du dépôt
┌─────────────────────────────────────────────────────────────────────────────┐│ GitHub Actions │├─────────────────────────────────────────────────────────────────────────────┤│ ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ │ Lint │ → │ Test │ → │ SAST │ → │ Audit │ ││ │ ruff │ │ pytest │ │ bandit │ │pip-audit │ ││ │ + in-toto│ │ + in-toto│ │ │ │ │ ││ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ ││ │ │ │ │ ││ └──────────────┴──────────────┴──────────────┘ ││ ↓ ││ ┌──────────────────────────────────────────────────────────────────────┐ ││ │ Build & Attest │ ││ │ Docker build → SLSA L3 attestation → SBOM (Syft) → Cosign sign │ ││ └──────────────────────────────────────────────────────────────────────┘ ││ ↓ ││ ┌──────────────────────────────────────────────────────────────────────┐ ││ │ GitHub Container Registry │ ││ │ ghcr.io/owner/app@sha256:... │ ││ │ ├── attestation SLSA L3 (in-toto format) │ ││ │ ├── signature Cosign (Sigstore keyless) │ ││ │ ├── SBOM SPDX + CycloneDX │ ││ │ └── links in-toto (lint, test, audit, build) │ ││ └──────────────────────────────────────────────────────────────────────┘ ││ ↓ ││ ┌────────────────────────┐ ┌────────────────────────┐ ││ │ GUAC │ │ Scorecard │ ││ │ Graphe dépendances │ │ Score sécurité repo │ ││ │ + certifier OSV │ │ Branch protection ✓ │ ││ │ → alertes CVE 0-day │ │ CODEOWNERS ✓ │ ││ └────────────────────────┘ └────────────────────────┘ │└─────────────────────────────────────────────────────────────────────────────┘Prérequis
Section intitulée « Prérequis »Avant de commencer, assurez-vous d'avoir les Connaissances préalables recommandées :
Si c'est le cas, allons-y ! Commençons pas installer les outils localement.
| Élément | Version minimale | Vérification | Installation |
|---|---|---|---|
| Git | 2.34+ | git --version | Guide Git |
| Python | 3.11+ | python --version | Guide pyenv |
| Docker | 24+ | docker --version | Guide Docker |
| Gitsign | 0.10+ | gitsign version | Guide Gitsign |
| Cosign | 2.0+ | cosign version | Guide Cosign |
| Syft | 1.0+ | syft version | Guide Syft |
| Trivy | 0.50+ | trivy version | Guide Trivy |
| slsa-verifier | 2.0+ | slsa-verifier version | GitHub |
Étape 1 : Créer le dépôt GitHub
Section intitulée « Étape 1 : Créer le dépôt GitHub »1.1 Initialiser le dépôt
Section intitulée « 1.1 Initialiser le dépôt »La première étape est de créer un dépôt GitHub correctement configuré. Les choix faits ici impactent directement votre score Scorecard.
-
Créer le dépôt : Allez sur github.com/new
-
Configurer les paramètres de base :
Paramètre Valeur recommandée Pourquoi Repository name secure-python-pipelineNom explicite Description "API Python avec pipeline CI/CD sécurisé" Aide au référencement Visibility Public Scorecard fonctionne mieux sur les dépôts publics Add README ✅ Oui Point d'entrée pour les contributeurs Add .gitignore Python Exclut __pycache__,.venv,.pytest_cacheLicense MIT ou Apache 2.0 Clarté juridique, check License Scorecard -
Cliquer sur "Create repository"
Cloner le dépôt et créer une branche de travail :
# Cloner le dépôt (remplacez VOTRE_USER par votre username GitHub)git clone https://github.com/VOTRE_USER/secure-python-pipeline.gitcd secure-python-pipeline
# Créer immédiatement une branche de travail# (les rulesets bloqueront les commits directs sur main)git checkout -b setup
# Vérifier que tout est en placels -la# Vous devriez voir : .git/ .gitignore LICENSE README.mdConfigurer Git localement (si pas déjà fait) :
# Configurer votre identité (utilisée pour les commits)git config user.name "Votre Nom"git config user.email "votre.email@example.com"Configurer Gitsign pour la signature keyless
Section intitulée « Configurer Gitsign pour la signature keyless »Pour ce guide, nous utilisons Gitsign, la signature keyless avec Sigstore. Contrairement à GPG ou SSH, vous n'avez aucune clé à gérer : vous vous authentifiez avec votre compte GitHub et Sigstore s'occupe du reste.
Installer Gitsign :
# macOSbrew install sigstore/tap/gitsign
# Linux (téléchargement direct)GITSIGN_VERSION=$(curl -s https://api.github.com/repos/sigstore/gitsign/releases/latest | grep tag_name | cut -d '"' -f 4)curl -LO "https://github.com/sigstore/gitsign/releases/download/${GITSIGN_VERSION}/gitsign_${GITSIGN_VERSION#v}_linux_amd64"chmod +x gitsign_*_linux_amd64sudo mv gitsign_*_linux_amd64 /usr/local/bin/gitsign
# Vérifier l'installationgitsign versionConfigurer Git pour utiliser Gitsign (dans ce projet uniquement) :
# Activer la signature automatique des commitsgit config --local commit.gpgsign true
# Utiliser le format x509 (requis pour Gitsign)git config --local gpg.format x509
# Spécifier Gitsign comme programme de signaturegit config --local gpg.x509.program gitsignTester la signature :
# Créer un fichier de testecho "test" > test.txtgit add test.txtgit commit -m "test: vérification signature gitsign"Un navigateur s'ouvre pour vous authentifier avec votre compte GitHub (ou Google/Microsoft). Après authentification :
- Fulcio délivre un certificat éphémère (~10 minutes)
- Votre commit est signé avec ce certificat
- La signature est enregistrée dans Rekor (log de transparence public)
Vérifier la signature :
git log --show-signature -1Sortie attendue :
commit abc123...gitsign: Signature made Mon Dec 30 10:00:00 2024 CETgitsign: Good signature from "votre-email@example.com"gitsign: WARNING: no matching key found in keyring This is expected for keyless signing1.2 Configurer les rulesets de branche
Section intitulée « 1.2 Configurer les rulesets de branche »Les rulesets (ensembles de règles) remplacent les anciennes "branch protection rules". Ils permettent de définir des contraintes sur les branches et tags. C'est une exigence SLSA Source L2 pour protéger l'intégrité du code source.
-
Accéder aux paramètres :
- Allez sur votre dépôt GitHub
- Cliquez sur Settings (onglet en haut à droite)
- Dans la sidebar gauche, section "Code and automation", cliquez sur Rules → Rulesets
-
Créer un nouveau ruleset :
- Cliquez sur New ruleset → New branch ruleset
- Ruleset name :
main-protection(nom descriptif) - Enforcement status : Active (appliqué immédiatement)
-
Configurer le ciblage (Target branches) :
- Cliquez sur Add target → Include default branch
- Cela cible automatiquement
main(oumasterselon votre config)
-
Configurer les règles de base :
Dans la section Rules, activez ces protections :
Règle Action Effet Check Scorecard Restrict deletions ✅ Cocher Empêche la suppression de la branche , Require linear history ✅ Cocher Force squash ou rebase (pas de merge commits) , Block force pushes ✅ Cocher (défaut) Empêche git push --force, -
Configurer "Require a pull request before merging" :
Cette règle est essentielle pour le check Code-Review de Scorecard.
- ✅ Cocher Require a pull request before merging
- Cliquer sur la flèche pour déplier les options :
Option Valeur Explication Required approvals 1Nombre minimum de reviewers (augmentez pour les projets critiques) Dismiss stale pull request approvals when new commits are pushed ✅ Invalide les approvals si le code change après review Require review from Code Owners ✅ Force l'approbation par les CODEOWNERS Require approval of the most recent reviewable push ✅ L'auteur du dernier push ne peut pas auto-approuver Require conversation resolution before merging ✅ Tous les commentaires doivent être résolus -
Configurer "Require status checks to pass before merging" :
Cette règle garantit que la CI passe avant tout merge.
- ✅ Cocher Require status checks to pass
- ✅ Cocher Require branches to be up to date before merging (mode strict)
- Cliquer sur Add checks et ajouter les jobs de votre CI :
lint(job Ruff)test(job Pytest)sast(job Bandit)audit(job pip-audit)
-
Configurer "Require signed commits" :
- ✅ Cocher Require signed commits
- Seuls les commits signés seront acceptés
GitHub reconnaît trois types de signatures :
Méthode Badge "Verified" Gestion de clés GPG ✅ Natif Clé à créer et protéger SSH ✅ Natif Utilise votre clé SSH Gitsign ⚠️ Via Rekor Aucune (keyless) -
Configurer le bypass (optionnel mais recommandé) :
Dans la section Bypass list, vous pouvez autoriser certains rôles à contourner les règles en cas d'urgence :
- Cliquez sur Add bypass → sélectionnez Repository admin
- Choisissez For pull requests only (pas de push direct, mais peut merger sans attendre)
⚠️ Attention : n'abusez pas des bypass, ils réduisent votre score Scorecard.
-
Sauvegarder :
- Cliquez sur Create en bas de la page
- Le ruleset est immédiatement actif
Récapitulatif des règles configurées :
┌─────────────────────────────────────────────────────────────────────────┐│ Ruleset "main-protection" │├─────────────────────────────────────────────────────────────────────────┤│ Target: default branch (main) ││ Status: Active │├─────────────────────────────────────────────────────────────────────────┤│ ✅ Restrict deletions ││ ✅ Require linear history ││ ✅ Block force pushes ││ ✅ Require pull request (1 approval, dismiss stale, CODEOWNERS) ││ ✅ Require status checks (lint, test, sast, audit + up-to-date) ││ ✅ Require signed commits │├─────────────────────────────────────────────────────────────────────────┤│ Bypass: Repository admin (pull requests only) │└─────────────────────────────────────────────────────────────────────────┘1.3 Activer Scorecard
Section intitulée « 1.3 Activer Scorecard »OpenSSF Scorecard analyse automatiquement votre dépôt et attribue un score de sécurité.
Scorecard fonctionne via une GitHub Action, pas d'app à installer. Le
workflow scorecard.yml sera ajouté à l'étape 4. Une fois en place :
- Les résultats apparaissent dans l'onglet Security > Code scanning alerts
- Votre score est visible sur scorecard.dev (dépôts publics)
1.4 Fichiers pour maximiser le score Scorecard
Section intitulée « 1.4 Fichiers pour maximiser le score Scorecard »Scorecard évalue plusieurs critères. Pour atteindre un score élevé (8+/10), vous devez ajouter ces fichiers.
CODEOWNERS (Code-Review check)
Section intitulée « CODEOWNERS (Code-Review check) »Le fichier CODEOWNERS définit qui doit approuver les modifications sur chaque
partie du code. C'est essentiel pour le check Code-Review.
.github/CODEOWNERS :
# Propriétaires par défaut pour tout le dépôt* @VOTRE_USER
# Sécurité : équipe sécurité doit approuver les workflows.github/workflows/ @VOTRE_USER.github/CODEOWNERS @VOTRE_USER
# Configuration DockerDockerfile @VOTRE_USER.dockerignore @VOTRE_USER
# Dépendances : revue obligatoirerequirements*.txt @VOTRE_USERpyproject.toml @VOTRE_USERSECURITY.md (Security-Policy check)
Section intitulée « SECURITY.md (Security-Policy check) »Le fichier SECURITY.md décrit comment signaler les vulnérabilités. C'est
obligatoire pour le check Security-Policy.
SECURITY.md :
# Politique de sécurité
## Versions supportées
| Version | Supportée || ------- | --------- || 1.x | ✅ || < 1.0 | ❌ |
## Signaler une vulnérabilité
**Ne créez PAS d'issue publique pour les vulnérabilités de sécurité.**
Utilisez plutôt le [signalement privé GitHub](../../security/advisories/new)ou envoyez un email à : `security@votre-domaine.com`
### Informations à inclure
- Description de la vulnérabilité- Étapes pour reproduire- Version affectée- Impact potentiel
### Délai de réponse
- Accusé de réception : 48h- Première évaluation : 7 jours- Correctif (selon sévérité) : 30-90 jours
## Pratiques de sécurité
Ce projet applique :
- Analyse statique (Bandit, Ruff)- Audit des dépendances (pip-audit)- Signature des images (Cosign/Sigstore)- Attestation SLSA L3- Scan de vulnérabilités (Trivy)Templates d'issues (bonnes pratiques)
Section intitulée « Templates d'issues (bonnes pratiques) »Les templates structurent les signalements et améliorent la qualité des issues.
.github/ISSUE_TEMPLATE/bug_report.yml :
name: "🐛 Bug Report"description: "Signaler un bug ou un comportement inattendu"labels: ["bug", "triage"]body: - type: markdown attributes: value: | Merci de prendre le temps de signaler ce bug !
- type: textarea id: description attributes: label: "Description du bug" description: "Décrivez clairement le problème rencontré" validations: required: true
- type: textarea id: steps attributes: label: "Étapes pour reproduire" description: "Comment reproduire le bug ?" placeholder: | 1. Aller sur '...' 2. Cliquer sur '...' 3. Voir l'erreur validations: required: true
- type: textarea id: expected attributes: label: "Comportement attendu" description: "Que devrait-il se passer ?" validations: required: true
- type: input id: version attributes: label: "Version" description: "Quelle version utilisez-vous ?" placeholder: "v1.0.0" validations: required: true
- type: dropdown id: os attributes: label: "Système d'exploitation" options: - Linux - macOS - Windows - Autre validations: required: true.github/ISSUE_TEMPLATE/feature_request.yml :
name: "✨ Feature Request"description: "Proposer une nouvelle fonctionnalité"labels: ["enhancement"]body: - type: markdown attributes: value: | Merci de proposer une amélioration !
- type: textarea id: problem attributes: label: "Problème à résoudre" description: "Quel problème cette fonctionnalité résoudrait-elle ?" validations: required: true
- type: textarea id: solution attributes: label: "Solution proposée" description: "Décrivez votre idée de solution" validations: required: true
- type: textarea id: alternatives attributes: label: "Alternatives considérées" description: "Avez-vous envisagé d'autres approches ?"
- type: textarea id: context attributes: label: "Contexte additionnel" description: "Captures d'écran, liens, exemples...".github/ISSUE_TEMPLATE/config.yml :
blank_issues_enabled: falsecontact_links: - name: "💬 Discussions" url: https://github.com/VOTRE_USER/secure-python-pipeline/discussions about: "Questions générales et discussions" - name: "📖 Documentation" url: https://github.com/VOTRE_USER/secure-python-pipeline#readme about: "Consultez la documentation avant de poser une question"Template de Pull Request
Section intitulée « Template de Pull Request »Le template PR guide les contributeurs et assure des descriptions complètes.
.github/PULL_REQUEST_TEMPLATE.md :
## Description
<!-- Décrivez vos changements en détail -->
## Type de changement
- [ ] 🐛 Bug fix (changement non-breaking qui corrige un problème)- [ ] ✨ Nouvelle fonctionnalité (changement non-breaking qui ajoute une fonction)- [ ] 💥 Breaking change (changement qui casse la compatibilité)- [ ] 📝 Documentation (mise à jour de la doc uniquement)- [ ] 🧪 Tests (ajout ou modification de tests)- [ ] 🔧 Configuration (CI, dépendances, config)
## Checklist
- [ ] Mon code suit les conventions du projet- [ ] J'ai testé mes changements localement- [ ] J'ai ajouté des tests si nécessaire- [ ] J'ai mis à jour la documentation si nécessaire- [ ] Mes commits suivent les [Conventional Commits](https://conventionalcommits.org)
## Tests effectués
<!-- Décrivez les tests que vous avez effectués -->
## Captures d'écran (si applicable)
<!-- Ajoutez des captures d'écran pour les changements visuels -->
## Issues liées
<!-- Utilisez "Fixes #123" ou "Closes #123" pour lier automatiquement -->CONTRIBUTING.md (Maintained check)
Section intitulée « CONTRIBUTING.md (Maintained check) »CONTRIBUTING.md :
# Guide de contribution
Merci de votre intérêt pour ce projet !
## Prérequis
- Python 3.11+- Docker 24+- Git
## Installation locale
```bashgit clone https://github.com/VOTRE_USER/secure-python-pipeline.gitcd secure-python-pipelinepython -m venv .venvsource .venv/bin/activatepip install -r requirements.txt -r requirements-dev.txt```
## Lancer les tests
```bashpytestruff check src/ tests/bandit -r src/```
## Processus de contribution
1. Forkez le projet2. Créez une branche (`git checkout -b feature/ma-feature`)3. Commitez vos changements (`git commit -m 'feat: ajoute ma feature'`)4. Poussez la branche (`git push origin feature/ma-feature`)5. Ouvrez une Pull Request
## Conventions
- **Commits** : suivez [Conventional Commits](https://conventionalcommits.org)- **Code** : formaté avec Ruff- **Tests** : couverture minimale 80%
## Code de conduite
Ce projet suit le [Contributor Covenant](https://www.contributor-covenant.org/).Soyez respectueux et inclusif.README.md (Documentation check)
Section intitulée « README.md (Documentation check) »Un bon README est essentiel pour le check Maintained et pour accueillir les contributeurs. Il doit inclure les badges de sécurité.
README.md :
# Secure Python Pipeline
[](https://github.com/VOTRE_USER/secure-python-pipeline/actions/workflows/ci.yml)[](https://scorecard.dev/viewer/?uri=github.com/VOTRE_USER/secure-python-pipeline)[](https://www.bestpractices.dev/projects/XXXXX)
API Python de démonstration avec pipeline CI/CD hautement sécurisé.
## 🔒 Sécurité
Ce projet implémente les bonnes pratiques de supply chain security :
| Protection | Outil | Status ||------------|-------|--------|| Attestation SLSA L3 | GitHub Attestations | ✅ || Signature d'image | Cosign (Sigstore) | ✅ || SBOM | Syft (SPDX + CycloneDX) | ✅ || Scan vulnérabilités | Trivy, pip-audit | ✅ || Analyse statique | Bandit, Ruff | ✅ || Dépendances épinglées | Hash pinning | ✅ |
### Vérifier l'image
```bash# Vérifier la signature Cosigncosign verify \ --certificate-identity-regexp="https://github.com/VOTRE_USER/secure-python-pipeline" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ ghcr.io/VOTRE_USER/secure-python-pipeline:latest
# Vérifier l'attestation SLSAgh attestation verify oci://ghcr.io/VOTRE_USER/secure-python-pipeline:latest \ --owner VOTRE_USER```
## 🚀 Démarrage rapide
### Prérequis
- Python 3.11+- Docker 24+
### Installation locale
```bashgit clonepython -m venv .venvsource .venv/bin/activatepip install -r requirements.txt```
### Lancer l'application
```bashuvicorn app.main:app --reload```
L'API est disponible sur http://localhost:8000
### Avec Docker
```bashdocker pull ghcr.io/VOTRE_USER/secure-python-pipeline:latestdocker run -p 8000:8000 ghcr.io/VOTRE_USER/secure-python-pipeline:latest```
## 📚 API
| Endpoint | Méthode | Description ||----------|---------|-------------|| `/` | GET | Message de bienvenue || `/health` | GET | Health check pour Kubernetes |
## 🧪 Tests
```bash# Installer les dépendances de devpip install -r requirements-dev.txt
# Lancer les testspytest
# Lintingruff check src/ tests/
# Analyse de sécuritébandit -r src/```
## 📝 Licence
Sous licence MIT - voir le fichier `LICENSE` à la racine du dépôt.
## 🤝 Contribuer
Voir le fichier `CONTRIBUTING.md` à la racine du dépôt.
## 🔐 Signaler une vulnérabilité
Voir le fichier `SECURITY.md` à la racine du dépôt.Récapitulatif des checks Scorecard
Section intitulée « Récapitulatif des checks Scorecard »Ce tableau récapitule les checks Scorecard couverts par les fichiers et la configuration ajoutés jusqu'ici, et signale ceux qui restent optionnels.
| Check | Fichier/Config | Impact |
|---|---|---|
| Branch-Protection | Règles de branche | ✅ Déjà fait |
| Code-Review | CODEOWNERS + branch rules | ✅ |
| Security-Policy | SECURITY.md | ✅ |
| Dependency-Update-Tool | dependabot.yml | ✅ Déjà fait |
| Pinned-Dependencies | SHA dans workflows | ✅ Déjà fait |
| Token-Permissions | permissions: read | ✅ Déjà fait |
| SAST | Bandit dans CI | ✅ Déjà fait |
| Signed-Releases | Cosign signature | ✅ Déjà fait |
| Vulnerabilities | Trivy, pip-audit | ✅ Déjà fait |
| Maintained | Commits récents, CONTRIBUTING | ✅ |
| License | LICENSE file | ✅ Déjà fait |
| Dangerous-Workflow | Pas de pull_request_target | ✅ Déjà fait |
| Fuzzing | OSS-Fuzz (optionnel) | ⚠️ Avancé |
| CII-Best-Practices | Badge OpenSSF | ⚠️ Optionnel |
Étape 2 : Structure du projet Python
Section intitulée « Étape 2 : Structure du projet Python »2.1 Arborescence cible
Section intitulée « 2.1 Arborescence cible »Voici la structure finale du projet :
Répertoire.github/
- CODEOWNERS
RépertoireISSUE_TEMPLATE/
- bug_report.yml
- feature_request.yml
- config.yml
- PULL_REQUEST_TEMPLATE.md
- dependabot.yml
Répertoireworkflows/
- ci.yml
- release.yml
- scorecard.yml
Répertoiresrc/
Répertoireapp/
- __init__.py
- main.py
Répertoiretests/
- __init__.py
- test_main.py
- .dockerignore
- .gitignore ← CRITIQUE : doit contenir .venv/
- .python-version
- CONTRIBUTING.md
- Dockerfile
- pyproject.toml
- requirements.txt
- requirements-dev.txt
- SECURITY.md
2.2 Créer l'application Python
Section intitulée « 2.2 Créer l'application Python »src/app/main.py : une API FastAPI minimaliste :
"""API de démonstration pour pipeline sécurisé."""from fastapi import FastAPI
app = FastAPI( title="Secure Python Pipeline Demo", version="1.0.0", description="Application de démonstration avec supply chain sécurisée",)
@app.get("/")async def root() -> dict[str, str]: """Endpoint racine.""" return {"message": "Hello, Secure World!"}
@app.get("/health")async def health() -> dict[str, str]: """Endpoint de santé pour les probes Kubernetes.""" return {"status": "healthy"}src/app/__init__.py : fichier vide pour le package :
"""Package principal de l'application."""tests/test_main.py : tests unitaires :
"""Tests pour l'API."""from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_root() -> None: """Test de l'endpoint racine.""" response = client.get("/") assert response.status_code == 200 assert response.json() == {"message": "Hello, Secure World!"}
def test_health() -> None: """Test de l'endpoint de santé.""" response = client.get("/health") assert response.status_code == 200 assert response.json()["status"] == "healthy"tests/__init__.py : fichier vide :
"""Package de tests."""2.3 Configurer les dépendances avec hash
Section intitulée « 2.3 Configurer les dépendances avec hash »Les dépendances épinglées avec hash garantissent l'intégrité. Même si PyPI était compromis, pip refuserait d'installer un fichier modifié.
pyproject.toml : configuration du projet :
[project]name = "secure-python-pipeline"version = "1.0.0"description = "Démonstration pipeline CI/CD sécurisé"requires-python = ">=3.11"license = {text = "MIT"}
[build-system]requires = ["hatchling"]build-backend = "hatchling.build"
[tool.ruff]target-version = "py311"line-length = 88
[tool.ruff.lint]select = ["E", "F", "I", "S", "B", "C4", "UP"]
[tool.ruff.lint.per-file-ignores]# S101 = assert (normal dans les tests pytest)"tests/**/*.py" = ["S101"]
[tool.pytest.ini_options]testpaths = ["tests"]pythonpath = ["src"]
[tool.bandit]exclude_dirs = ["tests", ".venv"]requirements.txt, À GÉNÉRER, ne pas copier-coller :
# 1. Installer pip-toolspip install pip-tools
# 2. Créer requirements.in (fichier source sans hash)cat > requirements.in << EOFfastapiuvicornEOF
# 3. Générer requirements.txt avec TOUTES les dépendances et hashspip-compile --generate-hashes requirements.in -o requirements.txtExemple de sortie (extrait, le fichier complet fait 200-300 lignes) :
# This file is autogenerated by pip-compile with Python 3.11annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc879daf1b616d8de89816493b8fd5c794f47 # via pydanticanyio==4.7.0 \ --hash=sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94 # via starlette# ... (50+ autres packages avec leurs hashs)fastapi==0.115.6 \ --hash=sha256:... (hash du jour)# ... etcrequirements-dev.txt : dépendances de développement :
# Outils de qualitéruff>=0.8.0bandit>=1.7.0pip-audit>=2.7.0
# Testspytest>=8.0.0pytest-cov>=6.0.0 # Plugin coverage pour pytesthttpx>=0.27.0 # Pour TestClient FastAPI
# Buildpip-tools>=7.4.02.4 Créer le .gitignore
Section intitulée « 2.4 Créer le .gitignore »Critique : Le dossier .venv/ ne doit jamais être committé. Il
contient des binaires spécifiques à votre système et fait échouer les checks
Scorecard.
.gitignore :
# Python runtime__pycache__/*.py[cod]*$py.class*.so.Python
# Environnements virtuels (CRITIQUE - ne jamais commiter).venv/venv/ENV/env/
# Outils Python.pytest_cache/.ruff_cache/.mypy_cache/htmlcov/.coverage.tox/dist/build/*.egg-info/
# IDE.vscode/.idea/*.swp*.swo
# OS.DS_StoreThumbs.db2.5 Créer le Dockerfile multi-stage
Section intitulée « 2.5 Créer le Dockerfile multi-stage »Un Dockerfile multi-stage réduit la surface d'attaque en n'incluant que le
strict nécessaire dans l'image finale. L'image de base est épinglée par
digest @sha256, comme les actions par SHA : c'est ce qui garantit un
build reproductible même si le tag python:3.11-slim est republié sur le
registre.
Dockerfile :
# syntax=docker/dockerfile:1
# =============================================================================# Stage 1 : Builder - Installer les dépendances dans un venv# =============================================================================FROM python:3.11-slim@sha256:a3ab0b966bc4e91546a033e22093cb840908979487a9fc0e6e38295747e49ac0 AS builder
# Éviter les fichiers .pyc et activer le mode unbufferedENV PYTHONDONTWRITEBYTECODE=1ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Copier uniquement les fichiers de dépendances d'abord (cache Docker)COPY requirements.txt .
# Créer un virtualenv et installer les dépendancesRUN python -m venv /opt/venvENV PATH="/opt/venv/bin:$PATH"
# --require-hashes force la vérification des hashRUN pip install --no-cache-dir --require-hashes -r requirements.txt
# =============================================================================# Stage 2 : Production - Image minimale# =============================================================================FROM python:3.11-slim@sha256:a3ab0b966bc4e91546a033e22093cb840908979487a9fc0e6e38295747e49ac0 AS production
# Labels OCI pour la traçabilité (utilisés par Syft pour le SBOM)LABEL org.opencontainers.image.source="https://github.com/VOTRE_USER/secure-python-pipeline"LABEL org.opencontainers.image.description="API Python avec supply chain sécurisée"LABEL org.opencontainers.image.licenses="MIT"
# Créer un utilisateur non-rootRUN groupadd --gid 1000 appgroup && \ useradd --uid 1000 --gid 1000 --shell /bin/false appuser
WORKDIR /app
# Copier le virtualenv depuis le builderCOPY --from=builder /opt/venv /opt/venvENV PATH="/opt/venv/bin:$PATH"
# Copier le code sourceCOPY --chown=appuser:appgroup src/ ./src/
# Passer en utilisateur non-rootUSER appuser
# Port exposé (documentation)EXPOSE 8000
# Healthcheck intégréHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Commande de démarrageCMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]2.6 Créer le .dockerignore
Section intitulée « 2.6 Créer le .dockerignore ».dockerignore : exclure les fichiers inutiles du contexte Docker :
# Git.git.gitignore
# Python__pycache__*.pyc*.pyo.venv.pytest_cache.ruff_cache.mypy_cache
# IDE.vscode.idea
# Tests et docstests/docs/*.md
# CI/CD.github/Étape 3 : Configurer Dependabot
Section intitulée « Étape 3 : Configurer Dependabot »Dependabot crée automatiquement des PR pour mettre à jour vos dépendances quand des vulnérabilités sont découvertes.
.github/dependabot.yml :
version: 2updates: # Dépendances Python - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" day: "monday" commit-message: prefix: "chore(deps):" labels: - "dependencies" - "python" # Grouper les mises à jour mineures groups: python-minor: patterns: - "*" update-types: - "minor" - "patch"
# Actions GitHub - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" day: "monday" commit-message: prefix: "chore(ci):" labels: - "dependencies" - "github-actions"
# Dockerfile - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" commit-message: prefix: "chore(docker):"Étape 4 : Créer les workflows GitHub Actions
Section intitulée « Étape 4 : Créer les workflows GitHub Actions »4.1 Workflow CI (tests et qualité)
Section intitulée « 4.1 Workflow CI (tests et qualité) »Ce workflow s'exécute sur chaque PR et commit sur main. Il vérifie la qualité du code avant le merge.
.github/workflows/ci.yml :
name: CI
on: push: branches: [main] pull_request: branches: [main]
# Aucun droit par défaut : chaque job demande le strict nécessairepermissions: {}
jobs: # =========================================================================== # Job 1 : Lint avec Ruff # =========================================================================== lint: name: Lint (Ruff) runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Set up Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11"
- name: Install Ruff run: pip install ruff==0.8.4
- name: Run Ruff formatter check run: ruff format --check src/ tests/
- name: Run Ruff linter run: ruff check src/ tests/
# =========================================================================== # Job 2 : Tests avec Pytest # =========================================================================== test: name: Test (Pytest) runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Set up Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11"
- name: Install dependencies run: | pip install --require-hashes -r requirements.txt pip install --require-hashes -r requirements-dev.txt
- name: Run tests with coverage run: pytest --cov=src/app --cov-report=xml
- name: Upload coverage to Codecov uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: files: coverage.xml fail_ci_if_error: false
# =========================================================================== # Job 3 : SAST avec Bandit # =========================================================================== sast: name: SAST (Bandit) runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Set up Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11"
- name: Install Bandit run: pip install bandit==1.8.0
- name: Run Bandit run: bandit -r src/ -f json -o bandit-report.json || true
- name: Upload Bandit report uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bandit-report path: bandit-report.json
# =========================================================================== # Job 4 : Audit des dépendances # =========================================================================== audit: name: Dependency Audit runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Set up Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11"
- name: Install pip-audit run: pip install pip-audit==2.7.3
- name: Run pip-audit run: pip-audit -r requirements.txt --strict
# =========================================================================== # Job 5 : Build Docker (sans push, juste vérification) # =========================================================================== build-check: name: Docker Build Check runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build image (no push) uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . push: false tags: test-build:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max4.2 Workflow Release (build, sign, publish)
Section intitulée « 4.2 Workflow Release (build, sign, publish) »Ce workflow se déclenche sur les tags v*. Il construit l'image avec
attestation SLSA, génère le SBOM et signe avec Cosign.
.github/workflows/release.yml :
name: Release
on: push: tags: - "v*"
# Aucun droit par défaut : chaque job demande le strict nécessairepermissions: {}
env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}
jobs: # =========================================================================== # Job 1 : Build, Attest, Sign, Push # =========================================================================== build-and-push: name: Build, Attest & Push runs-on: ubuntu-24.04
# Permissions spécifiques au job permissions: contents: read packages: write # Pour push vers GHCR id-token: write # Pour Sigstore OIDC attestations: write # Pour GitHub Attestations
outputs: digest: ${{ steps.build.outputs.digest }} image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to GitHub Container Registry uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=semver,pattern=v{{version}}
- name: Build and push image id: build uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: true # Génère l'attestation SLSA sbom: true # Génère le SBOM avec Syft
# ========================================================================= # Attestation GitHub (SLSA Build L3) # ========================================================================= - name: Generate SLSA attestation uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true
# ========================================================================= # Signature Cosign (keyless via Sigstore) # ========================================================================= - name: Install Cosign uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Sign image with Cosign env: DIGEST: ${{ steps.build.outputs.digest }} run: | IMAGE_REF="${REGISTRY}/${IMAGE_NAME}@${DIGEST}" echo "Signing: $IMAGE_REF" cosign sign --yes "$IMAGE_REF" echo "Verifying signature..." cosign verify \ --certificate-identity-regexp="https://github.com/${GITHUB_REPOSITORY}" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ "$IMAGE_REF"
# =========================================================================== # Job 2 : Générer SBOM détaillé avec Syft # =========================================================================== sbom: name: Generate SBOM runs-on: ubuntu-24.04 needs: build-and-push permissions: contents: read packages: write # Pour attacher le SBOM à l'image dans GHCR
steps: - name: Log in to GitHub Container Registry uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Syft uses: anchore/sbom-action/download-syft@a930d0ac434e3182448fe678398ba5713717112a # v0.21.0
- name: Generate SBOM (SPDX) env: IMAGE_REF: ${{ needs.build-and-push.outputs.image }}@${{ needs.build-and-push.outputs.digest }} run: syft "$IMAGE_REF" -o spdx-json=sbom-spdx.json
- name: Generate SBOM (CycloneDX) env: IMAGE_REF: ${{ needs.build-and-push.outputs.image }}@${{ needs.build-and-push.outputs.digest }} run: syft "$IMAGE_REF" -o cyclonedx-json=sbom-cyclonedx.json
- name: Upload SBOM artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: sbom path: | sbom-spdx.json sbom-cyclonedx.json
# Attacher le SBOM à l'image avec Cosign - name: Install Cosign uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Attach SBOM to image env: IMAGE_REF: ${{ needs.build-and-push.outputs.image }}@${{ needs.build-and-push.outputs.digest }} run: cosign attach sbom --sbom sbom-spdx.json "$IMAGE_REF"
# =========================================================================== # Job 3 : Scanner les vulnérabilités avec Trivy # =========================================================================== scan: name: Vulnerability Scan runs-on: ubuntu-24.04 needs: build-and-push permissions: packages: read # Pour récupérer l'image depuis GHCR security-events: write # Pour publier le rapport SARIF
steps: - name: Run Trivy scanner uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: "${{ needs.build-and-push.outputs.image }}@${{ needs.build-and-push.outputs.digest }}" format: "sarif" output: "trivy-results.sarif" severity: "CRITICAL,HIGH"
- name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: sarif_file: "trivy-results.sarif"4.3 Workflow CodeQL (analyse de sécurité statique)
Section intitulée « 4.3 Workflow CodeQL (analyse de sécurité statique) »Ce workflow analyse automatiquement votre code Python pour détecter des vulnérabilités de sécurité et des erreurs de qualité avec CodeQL.
.github/workflows/codeql.yml :
name: CodeQL
on: push: branches: [ "main" ] pull_request: branches: [ "main" ] schedule: # Analyser tous les lundis à 6h - cron: '0 6 * * 1'
# Aucun droit par défaut : le job demande le strict nécessairepermissions: {}
jobs: analyze: name: Analyze runs-on: ubuntu-24.04
permissions: security-events: write actions: read contents: read
strategy: fail-fast: false matrix: language: [ 'python' ]
steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Initialize CodeQL uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: languages: ${{ matrix.language }} queries: security-extended,security-and-quality
- name: Autobuild uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
- name: Perform CodeQL Analysis uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: category: "/language:${{ matrix.language }}"4.4 Workflow Scorecard
Section intitulée « 4.4 Workflow Scorecard »Ce workflow évalue les pratiques de sécurité de votre dépôt et publie les résultats sur OpenSSF Scorecard.
.github/workflows/scorecard.yml :
name: Scorecard
on: # Exécution hebdomadaire schedule: - cron: "0 6 * * 1" # Exécution sur push main (pour mise à jour rapide) push: branches: [main]
# Aucun droit par défaut : le job demande le strict nécessairepermissions: {}
jobs: analysis: name: Scorecard Analysis runs-on: ubuntu-24.04 permissions: contents: read security-events: write id-token: write # Pour publier sur scorecard.dev
steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Run Scorecard uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif publish_results: true
- name: Upload results to GitHub Security uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: sarif_file: results.sarif4.5 Workflow de vérification SLSA
Section intitulée « 4.5 Workflow de vérification SLSA »Ce workflow vérifie automatiquement les attestations SLSA de vos releases, assurant qu'elles sont valides et n'ont pas été compromises.
.github/workflows/verify-slsa.yml :
name: SLSA Verification
on: schedule: # Vérifier toutes les nuits - cron: '0 2 * * *' workflow_dispatch:
# Aucun droit par défaut : le job demande le strict nécessairepermissions: {}
jobs: verify: runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Install slsa-verifier run: | curl -LO https://github.com/slsa-framework/slsa-verifier/releases/latest/download/slsa-verifier-linux-amd64 chmod +x slsa-verifier-linux-amd64 sudo mv slsa-verifier-linux-amd64 /usr/local/bin/slsa-verifier
- name: Get latest tag id: latest env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | LATEST_TAG=$(gh api "repos/$GITHUB_REPOSITORY/tags" --jq '.[0].name') echo "tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
- name: Get image digest id: digest env: RELEASE_TAG: ${{ steps.latest.outputs.tag }} run: | DIGEST=$(docker pull "ghcr.io/$GITHUB_REPOSITORY:$RELEASE_TAG" 2>&1 | grep "Digest:" | cut -d' ' -f2) echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
- name: Verify SLSA with GitHub Attestations env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} IMAGE_DIGEST: ${{ steps.digest.outputs.digest }} run: | gh attestation verify "oci://ghcr.io/$GITHUB_REPOSITORY@$IMAGE_DIGEST" \ --owner "$GITHUB_REPOSITORY_OWNER"
- name: Report status if: always() env: JOB_STATUS: ${{ job.status }} RELEASE_TAG: ${{ steps.latest.outputs.tag }} run: | if [ "$JOB_STATUS" = "success" ]; then echo "Vérification SLSA réussie pour $RELEASE_TAG" else echo "Vérification SLSA échouée pour $RELEASE_TAG" exit 1 fiÉtape 5 : Premier déploiement
Section intitulée « Étape 5 : Premier déploiement »Vous avez travaillé dans la branche setup. Les rulesets bloquent les commits
directs sur main, c'est normal, c'est ce qu'on veut ! Nous allons merger
via une Pull Request en utilisant GitHub CLI.
-
Installer les dépendances :
Fenêtre de terminal # Créer un environnement virtuel Pythonpython -m venv .venvsource .venv/bin/activate # Linux/macOS# .venv\Scripts\activate # Windows# Mettre à jour pip (important pour éviter CVE-2025-8869)pip install --upgrade pip# Installer les dépendances de production et développementpip install -r requirements.txt -r requirements-dev.txt -
Tester localement avant de push :
Fenêtre de terminal # Formatter le code automatiquementruff format src/ tests/# Vérifier le lintingruff check src/ tests/# Lancer les testspytest tests/ -v# Vérifier les vulnérabilités des dépendancespip-audit# Scanner le code avec Banditbandit -r src/ -c pyproject.toml -
Commiter tous les fichiers dans la branche
setup:Fenêtre de terminal # Vérifiez que vous êtes sur la branche setupgit branch --show-current# Doit afficher : setup# ⚠️ CRITIQUE : Vérifier que .venv/ n'est PAS trackégit status --ignored# .venv/ doit apparaître sous "Ignored files"# Si .venv/ apparaît sous "Untracked" ou "Changes", c'est un problème !# Vérifier que .gitignore existe et contient .venv/grep -q "^\.venv/" .gitignore && echo "✓ .gitignore OK" || echo "❌ .gitignore manquant"git add .git commit -m "feat: initial secure pipeline setup"git push origin setup -
Créer une Pull Request avec gh :
Fenêtre de terminal gh pr create \--title "feat: initial secure pipeline setup" \--body "Setup initial du pipeline CI/CD sécurisé :- Configuration Gitsign (signature keyless)- Projet Python FastAPI avec tests- Workflows CI, Release et Scorecard- Fichiers Scorecard (CODEOWNERS, SECURITY.md, templates)- Dockerfile multi-stage sécurisé" \--base main -
Suivre l'exécution de la CI :
Fenêtre de terminal # Ouvrir la PR dans le navigateur pour voir les checksgh pr view --web# Ou suivre le statut en CLIgh pr checks --watch -
Merger la PR (une fois les checks verts) :
Fenêtre de terminal gh pr merge --squash --delete-branch -
Mettre à jour votre copie locale :
Fenêtre de terminal git checkout maingit reset --hard origin/main -
Créer le premier tag :
Fenêtre de terminal git tag v1.0.0git push origin v1.0.0 -
Vérifier le workflow Release :
Fenêtre de terminal # Suivre l'exécution du workflow Releasegh run watch -
Vérifier Scorecard : Après quelques minutes, votre score apparaît sur scorecard.dev
5.1 Débugger un score Scorecard bas
Section intitulée « 5.1 Débugger un score Scorecard bas »Si votre score Scorecard est inférieur à 8/10, exécutez Scorecard localement pour identifier les problèmes :
# Installer Scorecard CLIgo install github.com/ossf/scorecard/v5/cmd/scorecard@latest
# Scanner votre reposcorecard --repo=github.com/VOTRE_USER/secure-python-pipeline --show-detailsProblèmes courants et solutions
Section intitulée « Problèmes courants et solutions »| Check | Score | Cause | Solution |
|---|---|---|---|
| Token-Permissions | 0/10 | permissions: read-all ou permissions: {} | Définir permissions: contents: read explicitement au niveau global de chaque workflow |
| Binary-Artifacts | Erreur | .venv/ committé | Ajouter .venv/ au .gitignore et supprimer du repo |
| Pinned-Dependencies | Erreur | .venv/ committé | Même solution |
| SAST | Erreur | .venv/ committé | Même solution |
| Fuzzing | 0/10 | Pas de fuzzing | Optionnel, voir section fuzzing ci-dessous |
Fixer Token-Permissions :
Tous vos workflows doivent avoir permissions minimales :
# ❌ Mauvais - aucun bloc permissions : le jeton hérite des droits par défaut# (pas de permissions: déclaré)
# ❌ Mauvais - read-all accorde la lecture sur toutes les portéespermissions: read-all
# ✅ Bon - explicite et minimalpermissions: contents: readFixer Binary-Artifacts / Pinned-Dependencies / SAST :
Ces erreurs viennent du même problème : .venv/ ne doit jamais être
committé.
# 1. Ajouter au .gitignoreecho ".venv/" >> .gitignore
# 2. Supprimer du repogit rm -r --cached .venv/git commit -m "fix: remove venv from git"git push
# 3. Re-scannerscorecard --repo=github.com/VOTRE_USER/secure-python-pipeline --show-detailsExemple de .gitignore complet :
# Python__pycache__/*.py[cod]*$py.class*.so.Python.pytest_cache/.ruff_cache/.mypy_cache/dist/build/*.egg-info/.venv/venv/ENV/env/
# IDE.vscode/.idea/*.swp*.swo
# OS.DS_StoreThumbs.dbÉtape 6 : Vérifier les artefacts
Section intitulée « Étape 6 : Vérifier les artefacts »Maintenant que l'image est publiée, vérifions que tout est correct.
6.1 Vérifier la signature Cosign
Section intitulée « 6.1 Vérifier la signature Cosign »La signature Cosign prouve que l'image vient bien de votre workflow de
release. La commande cosign verify contrôle le certificat keyless contre
l'identité GitHub attendue : si l'image a été substituée, la vérification
échoue.
# Définir les variablesexport IMAGE=ghcr.io/VOTRE_USER/secure-python-pipelineexport TAG=v1.0.0
# Vérifier la signaturecosign verify \ --certificate-identity-regexp="https://github.com/VOTRE_USER/secure-python-pipeline" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ ${IMAGE}:${TAG}Sortie attendue :
Verification for ghcr.io/VOTRE_USER/secure-python-pipeline:v1.0.0 --The following checks were performed on each of these signatures: - The cosign claims were validated - Existence of the claims in the transparency log was verified - The code-signing certificate was verified using trusted certificate authority certificates
[{"critical":{"identity":{"docker-reference":"ghcr.io/..."},...}]6.2 Vérifier l'attestation SLSA
Section intitulée « 6.2 Vérifier l'attestation SLSA »L'attestation SLSA va plus loin que la signature : elle relie l'image au
commit source et au workflow qui l'a construite. La commande
gh attestation verify valide cette provenance auprès de l'API GitHub.
# Récupérer le digestDIGEST=$(crane digest ${IMAGE}:${TAG})
# Vérifier l'attestation SLSAgh attestation verify oci://${IMAGE}@${DIGEST} \ --owner VOTRE_USERSortie attendue :
✓ Verification succeeded!
sha256:abc123... was attested by a]Sigstore entry with ID 123456786.3 Inspecter le SBOM
Section intitulée « 6.3 Inspecter le SBOM »Le SBOM attaché à l'image liste l'ensemble de ses composants. L'inspecter confirme qu'il a bien été généré et permet de repérer une dépendance inattendue avant de déployer.
# Télécharger le SBOM attachécosign download sbom ${IMAGE}:${TAG} > sbom.json
# Voir le contenu (jq pour formater)cat sbom.json | jq '.packages[:5]'6.4 Scanner localement avec Trivy
Section intitulée « 6.4 Scanner localement avec Trivy »Le scan exécuté en CI peut dater de plusieurs heures. Relancer Trivy en local donne l'état des vulnérabilités connues au moment précis où vous contrôlez l'image, juste avant un déploiement.
# Scanner l'image pour vulnérabilitéstrivy image ${IMAGE}:${TAG}
# Scanner uniquement les CRITICAL et HIGHtrivy image --severity CRITICAL,HIGH ${IMAGE}:${TAG}Étape 7 : Intégrer GUAC pour la visibilité globale
Section intitulée « Étape 7 : Intégrer GUAC pour la visibilité globale »Vous avez maintenant des SBOM et des attestations pour chaque image. Mais si vous gérez 50 services, comment répondre rapidement à : "Quels services utilisent la dépendance vulnérable CVE-2024-XXXX ?"
GUAC (Graph for Understanding Artifact Composition) agrège tous vos SBOM et attestations dans un graphe de connaissances interrogeable. C'est le centre de contrôle de votre supply chain.
7.1 Déployer GUAC localement
Section intitulée « 7.1 Déployer GUAC localement »Pour ce tutoriel, nous déployons GUAC en local. En production, vous le déploierez sur un serveur dédié ou dans Kubernetes.
# Cloner le dépôt GUACgit clone https://github.com/guacsec/guac.gitcd guac
# Démarrer tous les services avec Docker Composedocker compose up -d
# Vérifier que les services tournentdocker compose psServices disponibles :
| Service | Port | Usage |
|---|---|---|
| GraphQL API | 8080 | Requêtes programmatiques |
| Visualizer | 3000 | Interface web d'exploration |
7.2 Ajouter l'ingestion GUAC au workflow Release
Section intitulée « 7.2 Ajouter l'ingestion GUAC au workflow Release »Ajoutez ce job dans .github/workflows/release.yml après le job sbom :
# =========================================================================== # Job 4 : Ingérer dans GUAC # =========================================================================== guac-ingest: name: Ingest to GUAC runs-on: ubuntu-24.04 needs: [build-and-push, sbom] permissions: contents: read # Uniquement si vous avez un GUAC hébergé (décommentez et adaptez l'URL) # if: ${{ vars.GUAC_API_URL != '' }}
steps: - name: Download SBOM artifacts uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: sbom
- name: Install guacone CLI run: | curl -sSL https://github.com/guacsec/guac/releases/latest/download/guacone-linux-amd64 \ -o /usr/local/bin/guacone chmod +x /usr/local/bin/guacone
- name: Ingest SBOM to GUAC env: GUAC_API_URL: ${{ vars.GUAC_API_URL || 'http://localhost:8080/query' }} run: | # Ingérer le SBOM SPDX guacone collect files sbom-spdx.json --gql-addr "$GUAC_API_URL"
# Ingérer le SBOM CycloneDX guacone collect files sbom-cyclonedx.json --gql-addr "$GUAC_API_URL"
- name: Ingest image from OCI registry env: GUAC_API_URL: ${{ vars.GUAC_API_URL || 'http://localhost:8080/query' }} IMAGE_REF: ${{ needs.build-and-push.outputs.image }}@${{ needs.build-and-push.outputs.digest }} run: | # Ingérer directement depuis le registry (récupère SBOM + attestations attachés) guacone collect oci "$IMAGE_REF" --gql-addr "$GUAC_API_URL"7.3 Ingérer manuellement les artefacts
Section intitulée « 7.3 Ingérer manuellement les artefacts »Si vous n'avez pas de GUAC hébergé, ingérez manuellement après chaque release :
# Variablesexport IMAGE=ghcr.io/VOTRE_USER/secure-python-pipelineexport TAG=v1.0.0export GUAC_API=http://localhost:8080/query
# Télécharger le SBOM depuis les artifacts GitHubgh run download --name sbom
# Ingérer les SBOMguacone collect files sbom-spdx.json --gql-addr ${GUAC_API}guacone collect files sbom-cyclonedx.json --gql-addr ${GUAC_API}
# Ingérer depuis le registry OCI (récupère aussi les attestations)guacone collect oci ${IMAGE}:${TAG} --gql-addr ${GUAC_API}7.4 Requêter GUAC
Section intitulée « 7.4 Requêter GUAC »Via l'interface web : Ouvrez http://localhost:3000 et explorez le graphe.
Via GraphQL : Trouvez tous les packages vulnérables dans votre image :
curl -s http://localhost:8080/query \ -H "Content-Type: application/json" \ -d '{ "query": "query { CertifyVuln(certifyVulnSpec: {}) { package { namespaces { names { name } } } vulnerability { vulnerabilityIDs { vulnerabilityID } } } }" }' | jq '.data.CertifyVuln[] | {package: .package.namespaces[0].names[0].name, cve: .vulnerability.vulnerabilityIDs[0].vulnerabilityID}'Exemple de sortie :
{"package": "requests", "cve": "CVE-2023-32681"}{"package": "starlette", "cve": "CVE-2024-12345"}7.5 Cas d'usage concrets avec GUAC
Section intitulée « 7.5 Cas d'usage concrets avec GUAC »| Question | Requête GUAC |
|---|---|
| "Quelles images utilisent log4j ?" | Rechercher IsDependencyOf avec package log4j |
| "Cette CVE nous affecte-t-elle ?" | CertifyVuln filtré par CVE ID |
| "D'où vient cette dépendance ?" | Traverser HasSourceAt depuis le package |
| "Cette image a-t-elle un SBOM ?" | Vérifier HasSBOM pour l'artifact |
| "Provenance SLSA de cette image ?" | Requêter HasSlsa |
Étape 8 : (Optionnel) Ajouter le fuzzing
Section intitulée « Étape 8 : (Optionnel) Ajouter le fuzzing »Le fuzzing teste votre application avec des entrées aléatoires pour détecter des bugs inattendus (crashes, comportements anormaux). C'est optionnel pour un projet simple, mais améliore votre score Scorecard.
Pourquoi fuzzer ?
Section intitulée « Pourquoi fuzzer ? »Le fuzzing découvre :
- Crashes : entrées qui font planter l'application
- Exceptions non gérées : cas limites oubliés
- Problèmes de sécurité : buffer overflows, injections, parsing errors
Solution rapide : Atheris (Python fuzzing)
Section intitulée « Solution rapide : Atheris (Python fuzzing) »Atheris est un fuzzer Python basé sur libFuzzer.
tests/fuzz_main.py :
"""Fuzzing de l'API avec Atheris."""import atherisimport sys
with atheris.instrument_imports(): from fastapi.testclient import TestClient from app.main import app
def test_one_input(data: bytes) -> None: """Fuzz test pour l'API.""" client = TestClient(app) try: # Tester l'endpoint racine avec des données aléatoires client.get("/", params={"input": data.decode("utf-8", errors="ignore")}) except (ValueError, UnicodeDecodeError): # Erreurs attendues - pas un bug pass
if __name__ == "__main__": atheris.Setup(sys.argv, test_one_input) atheris.Fuzz()Ajouter à requirements-dev.txt :
atheris>=2.3.0Ajouter à .github/workflows/ci.yml :
# =========================================================================== # Job 6 : Fuzzing (optionnel) # =========================================================================== fuzz: name: Fuzzing runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Set up Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11"
- name: Install dependencies run: | pip install -r requirements.txt -r requirements-dev.txt
- name: Run fuzzing (60 seconds) run: | python tests/fuzz_main.py -max_total_time=60Tester localement :
# Installer atherispip install atheris
# Lancer le fuzzing pendant 10 secondespython tests/fuzz_main.py -max_total_time=10Sortie attendue :
INFO: Running with entropic power schedule (0xFF, 100).INFO: Seed: 1234567890INFO: -max_len is not provided; libFuzzer will guess a good value based on the corpus.#2 INITED exec/s: 0 rss: 45Mb#1024 pulse exec/s: 512 rss: 48Mb...Done 50000 runs in 10 second(s)Si le fuzzing trouve un crash, Atheris l'affichera avec l'entrée qui a causé le problème.
Alternative : OSS-Fuzz (projets critiques)
Section intitulée « Alternative : OSS-Fuzz (projets critiques) »Pour les projets open source largement utilisés, vous pouvez rejoindre OSS-Fuzz, le fuzzing continu de Google :
- Suivre le guide d'intégration
- Google exécute le fuzzing 24/7 sur vos commits
- Vous recevez des rapports de bugs privés
Critères OSS-Fuzz :
- Projet open source avec > 10k utilisateurs ou dépendance critique
- Responsable de sécurité actif (SECURITY.md + réponses rapides)
- CI/CD configuré
Étape 9 : Politique in-toto pour la chaîne complète
Section intitulée « Étape 9 : Politique in-toto pour la chaîne complète »Jusqu'ici, nous avons des attestations SLSA qui prouvent que le build a eu lieu sur GitHub Actions. Mais comment garantir que toutes les étapes obligatoires (lint, test, scan) ont bien été exécutées avant la release ?
in-toto permet de définir un layout (politique) qui décrit les étapes requises. À chaque étape, un link (attestation) est généré. À la fin, la vérification compare les links au layout : si une étape manque ou si les hachages ne correspondent pas, l'artefact est rejeté.
9.1 Pourquoi aller au-delà de SLSA ?
Section intitulée « 9.1 Pourquoi aller au-delà de SLSA ? »| Ce que SLSA prouve | Ce qu'in-toto ajoute |
|---|---|
| Le build vient de GitHub Actions | Toutes les étapes définies ont été exécutées |
| Le commit source est identifié | Les fichiers en entrée/sortie correspondent |
| Le builder est authentifié | Une politique signée définit les règles |
Scénario d'attaque bloqué : un attaquant compromet le workflow et commente l'étape de scan de vulnérabilités. Avec SLSA seul, l'image est quand même signée. Avec in-toto, la vérification échoue car le link "scan" est manquant.
9.2 Installer in-toto
Section intitulée « 9.2 Installer in-toto »# Installation via pippip install in-toto
# Vérifier l'installationin-toto-run --versionin-toto-verify --version9.3 Créer un layout (politique)
Section intitulée « 9.3 Créer un layout (politique) »Le layout définit les étapes obligatoires et les clés autorisées. Créez un fichier layout.json :
{ "_type": "layout", "expires": "2026-12-31T23:59:59Z", "readme": "Layout pour secure-python-pipeline", "keys": {}, "steps": [ { "name": "lint", "expected_command": [], "threshold": 1, "pubkeys": [], "expected_materials": [ ["MATCH", "*.py", "WITH", "PRODUCTS", "FROM", "lint"] ], "expected_products": [] }, { "name": "test", "expected_command": [], "threshold": 1, "pubkeys": [], "expected_materials": [ ["MATCH", "*.py", "WITH", "PRODUCTS", "FROM", "lint"] ], "expected_products": [] }, { "name": "audit", "expected_command": [], "threshold": 1, "pubkeys": [], "expected_materials": [], "expected_products": [] }, { "name": "build", "expected_command": [], "threshold": 1, "pubkeys": [], "expected_materials": [ ["MATCH", "*.py", "WITH", "PRODUCTS", "FROM", "test"], ["MATCH", "Dockerfile", "WITH", "PRODUCTS", "FROM", "test"] ], "expected_products": [ ["CREATE", "image.tar"] ] } ], "inspect": []}9.4 Générer des links dans le workflow
Section intitulée « 9.4 Générer des links dans le workflow »Modifiez votre workflow CI pour générer un link à chaque étape :
jobs: lint: runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Setup Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.11'
- name: Install in-toto run: pip install in-toto
- name: Run lint with in-toto run: | in-toto-run \ --step-name lint \ --products src/*.py \ --no-command \ -- ruff check src/
- name: Upload link uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: link-lint path: lint.link
test: needs: lint runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false
- name: Setup Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.11'
- name: Install dependencies run: pip install in-toto pytest
- name: Download lint link uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: link-lint
- name: Run tests with in-toto run: | in-toto-run \ --step-name test \ --materials src/*.py \ --products src/*.py \ --no-command \ -- pytest tests/
- name: Upload link uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: link-test path: test.link9.5 Vérifier la chaîne complète
Section intitulée « 9.5 Vérifier la chaîne complète »Après le build, vérifiez que tous les links correspondent au layout :
# Télécharger tous les linksgh run download --name link-lintgh run download --name link-testgh run download --name link-auditgh run download --name link-build
# Vérifier contre le layoutin-toto-verify \ --layout layout.json \ --layout-keys owner-pubkey.pem \ --link-dir .Résultat attendu :
The software product passed all verification.Si une étape manque :
in-toto.exceptions.LinkNotFoundError: No link found for step 'audit'9.6 Intégration avec SLSA et GUAC
Section intitulée « 9.6 Intégration avec SLSA et GUAC »Les attestations in-toto sont compatibles avec l'écosystème Sigstore :
# Les attestations SLSA sont au format in-totocosign download attestation ghcr.io/VOTRE_USER/secure-python-pipeline:v1.0.0 \ | jq -r '.payload' | base64 -d | jq '._type'# Sortie : "https://in-toto.io/Statement/v1"
# GUAC peut ingérer les links in-totoguacone collect files *.link --gql-addr http://localhost:8080/queryDépannage
Section intitulée « Dépannage »Voici les erreurs les plus fréquentes rencontrées en construisant ce pipeline, avec leur cause probable et la correction à appliquer.
| Symptôme | Cause probable | Solution |
|---|---|---|
permission denied sur GHCR | Token sans scope packages:write | Vérifier les permissions du workflow |
| Cosign sign échoue | id-token: write manquant | Ajouter la permission dans le workflow |
| Attestation non trouvée | attestations: write manquant | Ajouter la permission |
| SBOM vide | Image sans packages détectables | Vérifier que Syft détecte le bon format |
| Scorecard score bas | Branch protection désactivée | Activer les protections recommandées |
| pip-audit échoue | Vulnérabilité dans une dépendance | Mettre à jour la dépendance concernée |
| GUAC ingestion échoue | URL GraphQL incorrecte | Vérifier GUAC_API_URL et la connectivité |
| GUAC ne voit pas les CVE | Certifier OSV pas lancé | Exécuter guacone certifier osv |
| in-toto verify échoue | Link manquant pour une étape | Vérifier que tous les jobs génèrent leur link |
LinkNotFoundError | Artefact link non téléchargé | Vérifier download-artifact dans le workflow |
Résumé des protections
Section intitulée « Résumé des protections »Le pipeline empile plusieurs couches de protection, chacune fermant une voie d'attaque différente, du code source jusqu'à la visibilité globale de la supply chain. Ce tableau les récapitule de bout en bout.
| Couche | Outil | Ce qu'il protège |
|---|---|---|
| Code | Ruff, Bandit | Qualité, vulnérabilités SAST |
| Dépendances | pip-audit, hash pinning | Vulnérabilités, intégrité |
| Build | SLSA attestation | Traçabilité du build |
| Image | Cosign signature | Authenticité de l'image |
| Inventaire | Syft SBOM | Liste des composants |
| Vulnérabilités | Trivy | CVE dans l'image |
| Repo | Scorecard | Pratiques de sécurité |
| Mises à jour | Dependabot | Dépendances obsolètes |
| Visibilité globale | GUAC | Graphe de toutes les dépendances et CVE |
| Politique pipeline | in-toto | Preuve que toutes les étapes ont été exécutées |
À retenir
Section intitulée « À retenir »- Branch protection est le premier rempart : pas de commit direct sur main
- Actions épinglées par SHA : les tags peuvent être modifiés, pas les SHA
- Hash pinning pour les dépendances : garantit l'intégrité même si PyPI est compromis
- SLSA attestation prouve que le build vient de votre CI, pas d'un laptop compromis
- Signature Cosign permet à vos consommateurs de vérifier l'authenticité
- SBOM documente exactement ce qui est dans l'image pour répondre aux audits
- GUAC connecte tous vos SBOM pour répondre rapidement aux CVE 0-day
- Scorecard vous donne une note et des axes d'amélioration concrets
- in-toto garantit que le pipeline complet a été respecté (lint → test → audit → build)
Prochaines étapes
Section intitulée « Prochaines étapes »Ressources externes
Section intitulée « Ressources externes »Pour approfondir chaque maillon de la chaîne, ces références officielles complètent le guide.