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.txtLancer les tests
Section intitulée « Lancer les tests »pytestruff check src/ tests/bandit -r src/Processus de contribution
Section intitulée « Processus de contribution »- Forkez le projet
- Créez une branche (
git checkout -b feature/ma-feature) - Commitez vos changements (
git commit -m 'feat: ajoute ma feature') - Poussez la branche (
git push origin feature/ma-feature) - Ouvrez une Pull Request
Conventions
Section intitulée « Conventions »- Commits : suivez Conventional Commits
- Code : formaté avec Ruff
- Tests : couverture minimale 80%
Code de conduite
Section intitulée « Code de conduite »Ce projet suit le Contributor Covenant. Soyez respectueux et inclusif.
#### README.md (Documentation check)
Un bon README est essentiel pour le check **Maintained** et pour accueillir lescontributeurs. Il doit inclure les badges de sécurité.
**`README.md`** :
````markdown# 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
Section intitulée « 🚀 Démarrage rapide »Prérequis
Section intitulée « Prérequis »- Python 3.11+
- Docker 24+
Installation locale
Section intitulée « Installation locale »git clonepython -m venv .venvsource .venv/bin/activatepip install -r requirements.txtLancer l'application
Section intitulée « Lancer l'application »uvicorn app.main:app --reloadL'API est disponible sur http://localhost:8000
Avec Docker
Section intitulée « Avec Docker »docker pull ghcr.io/VOTRE_USER/secure-python-pipeline:latestdocker run -p 8000:8000 ghcr.io/VOTRE_USER/secure-python-pipeline:latest| Endpoint | Méthode | Description |
|---|---|---|
/ | GET | Message de bienvenue |
/health | GET | Health check pour Kubernetes |
# 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
Section intitulée « 📝 Licence »🤝 Contribuer
Section intitulée « 🤝 Contribuer »Voir CONTRIBUTING.md
🔐 Signaler une vulnérabilité
Section intitulée « 🔐 Signaler une vulnérabilité »Voir SECURITY.md
#### Récapitulatif des checks Scorecard
Ce tableau récapitule les **checks Scorecard** couverts par les fichiers et laconfiguration 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 |
<Aside type="tip" title="Score cible">
Avec tous ces fichiers, vous devriez atteindre un score **8-9/10**. Les checksFuzzing et CII-Best-Practices sont optionnels et plus complexes à mettre en place.
</Aside>
## Étape 2 : Structure du projet Python
### 2.1 Arborescence cible
Voici la structure finale du projet :
<FileTree>
- .github/ - CODEOWNERS - ISSUE_TEMPLATE/ - bug_report.yml - feature_request.yml - config.yml - PULL_REQUEST_TEMPLATE.md - dependabot.yml - workflows/ - ci.yml - release.yml - scorecard.yml- src/ - app/ - \_\_init\_\_.py - main.py- tests/ - \_\_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
</FileTree>
<Aside type="caution" title="Le .gitignore est non négociable">
Le fichier `.gitignore` **doit** être créé **avant** le premier commit et**doit** contenir `.venv/`. Sans cela, vous risquez de commiter votreenvironnement virtuel, ce qui fera **échouer 3 checks Scorecard** et ferachuter votre score de ~9/10 à ~7/10.
Voir la section 2.4 ci-dessous pour le contenu exact du `.gitignore`.
</Aside>
### 2.2 Créer l'application Python
**`src/app/main.py`** — Une API FastAPI minimaliste :
```python"""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 :
```python"""Package principal de l'application."""```
**`tests/test_main.py`** — Tests unitaires :
```python"""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 :
```python"""Package de tests."""```
### 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 :
```toml[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** :
<Aside type="danger" title="NE PAS copier-coller ce fichier">
Les hashs ci-dessous sont **obsolètes**. Vous **devez** les régénérer avec`pip-compile`. Si vous copiez ce fichier tel quel, vous aurez une erreur`THESE PACKAGES DO NOT MATCH THE HASHES`.
</Aside>
```bash# 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.txt```
<Aside type="caution" title="Le fichier sera long">
`pip-compile` génère un `requirements.txt` avec **toutes** les dépendancestransitives (anyio, idna, sniffio, typing-extensions, etc.) et **tous** leurshashs. Le fichier fera **plusieurs centaines de lignes** — c'est normal.
**Exemple de début du fichier généré :**
```text# This file is autogenerated by pip-compile with Python 3.11# by the following command:## pip-compile --generate-hashes requirements.in#annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc879daf1b616d8de89816493b8fd5c794f47 \ --hash=sha256:aff07c09a53a08bc8cfcffe6d35c79b16d9bd46c9f8a963c74de09b9fb89ef34 # via pydanticanyio==4.7.0 \ --hash=sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94 \ --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 # via # httpcore # starlette# ... (beaucoup d'autres dépendances)```
Ne modifiez **pas** ce fichier manuellement. Utilisez toujours `pip-compile`.
</Aside>
**Exemple de sortie** (extrait — le fichier complet fait 200-300 lignes) :
```text# 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)# ... etc```
<Aside type="note" title="Ne pas copier cet exemple">
Ce n'est qu'un **aperçu**. Utilisez le fichier généré par `pip-compile` survotre machine — il contiendra les hashs corrects et complets.
</Aside>
**`requirements-dev.txt`** — Dépendances de développement :
```text# 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.0```
### 2.4 Créer le .gitignore
**Critique** : Le dossier `.venv/` ne doit **jamais** être committé. Ilcontient des binaires spécifiques à votre système et fait échouer les checksScorecard.
**`.gitignore`** :
```text# 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.db```
<Aside type="caution" title="Si vous avez déjà committé .venv/">
Si vous avez déjà committé `.venv/` par erreur :
```bash# Créer le .gitignore avec le contenu ci-dessuscat > .gitignore << 'EOF'.venv/# ... (copier le contenu complet ci-dessus)EOF
# Supprimer .venv/ de Git (mais pas du disque)git rm -r --cached .venv/
# Commitergit add .gitignoregit commit -m "fix: remove venv from git and add proper gitignore"```
**Pourquoi c'est critique ?** Scorecard tente de scanner tous les fichiers,y compris ceux dans `.venv/`. Les binaires Python dans `.venv/bin/` causentdes erreurs `path escapes from parent`, ce qui fait échouer les checksBinary-Artifacts, Pinned-Dependencies et SAST.
</Aside>
### 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 unbuild **reproductible** même si le tag `python:3.11-slim` est republié sur leregistre.
**`Dockerfile`** :
```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
**`.dockerignore`** — Exclure les fichiers inutiles du contexte Docker :
```text# 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
[Dependabot](https://docs.github.com/en/code-security/dependabot) créeautomatiquement des PR pour mettre à jour vos dépendances quand desvulnérabilités sont découvertes.
**`.github/dependabot.yml`** :
```yamlversion: 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):"```
<Aside type="tip" title="Auto-merge des PRs Dependabot">
**Option 1 : Merger manuellement avec gh CLI**
```bash# Lister toutes les PRs Dependabotgh pr list --author "app/dependabot"
# Merger toutes les PRs Dependabot qui passent la CIgh pr list --author "app/dependabot" --json number,title,statusCheckRollup \ --jq '.[] | select(.statusCheckRollup[]?.conclusion=="SUCCESS") | .number' \ | xargs -I {} gh pr merge {} --admin --squash --delete-branch
# Ou merger une seule PR spécifiquegh pr merge 5 --admin --squash --delete-branch```
**Option 2 : Workflow auto-merge (recommandé)**
Pour auto-merger les PRs Dependabot qui passent la CI, ajoutez ce workflow :
**`.github/workflows/dependabot-automerge.yml`** :
```yamlname: Dependabot Auto-merge
on: pull_request
# Aucun droit par défaut : le job demande le strict nécessairepermissions: {}
jobs: automerge: runs-on: ubuntu-24.04 # On vérifie l'auteur de la PR, pas github.actor (usurpable lors d'un re-run) if: github.event.pull_request.user.login == 'dependabot[bot]' permissions: contents: write pull-requests: write steps: - name: Enable auto-merge env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh pr merge --auto --squash "$PR_URL"```
Les PRs Dependabot seront automatiquement mergées dès que la CI passe.
**⚠️ Important** : cette approche fonctionne **uniquement si vous n'exigezpas d'approbation** pour les PRs, ou si vous configurez le bypass pourDependabot dans les rulesets.
</Aside>
## Étape 4 : Créer les workflows GitHub Actions
### 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`** :
```yamlname: 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=max```
<Aside type="note" title="Actions épinglées par SHA">
Remarquez que chaque action est épinglée par son **SHA de commit complet**(`@8e8c483db84b4bee98b60c0593521ed34d9990e8`) plutôt que par tag (`@v6`),suivi d'un **commentaire de version** pour la lisibilité. C'est une exigence**SLSA Build L3** : les dépendances de build doivent être **immuables**. Leguide [Épingler par SHA](/docs/pipeline-cicd/github/securite/pinner-sha/)détaille la méthode.
</Aside>
### 4.2 Workflow Release (build, sign, publish)
Ce workflow se déclenche sur les tags `v*`. Il construit l'image avecattestation SLSA, génère le SBOM et signe avec Cosign.
**`.github/workflows/release.yml`** :
```yamlname: 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)
Ce workflow analyse automatiquement votre code Python pour détecter desvulnérabilités de sécurité et des erreurs de qualité avec CodeQL.
**`.github/workflows/codeql.yml`** :
```yamlname: 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 }}"```
<Aside type="note" title="Queries étendues">
`queries: security-extended,security-and-quality` active les requêtes avancéesde CodeQL, détectant plus de problèmes que l'analyse standard. Cela augmentelégèrement le temps d'analyse mais améliore la couverture sécurité.
</Aside>
### 4.4 Workflow Scorecard
Ce workflow évalue les pratiques de sécurité de votre dépôt et publie lesrésultats sur [OpenSSF Scorecard](https://scorecard.dev).
**`.github/workflows/scorecard.yml`** :
```yamlname: 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.sarif```
### 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`** :
```yamlname: 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```
<Aside type="tip" title="Vérification continue">
Ce workflow s'exécute quotidiennement pour détecter toute modification oucompromission des attestations SLSA après publication. C'est une couche desécurité supplémentaire qui vérifie l'intégrité de vos releases dans le temps.
</Aside>
## Étape 5 : Premier déploiement
Vous avez travaillé dans la branche `setup`. Les rulesets bloquent les commitsdirects sur `main` — c'est normal, c'est ce qu'on veut ! Nous allons mergervia une Pull Request en utilisant **GitHub CLI**.
<Steps>
1. **Installer les dépendances** :
```bash # Créer un environnement virtuel Python python -m venv .venv source .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éveloppement pip install -r requirements.txt -r requirements-dev.txt ```
<Aside type="caution" title="CVE pip">
Si `pip-audit` vous alerte sur une CVE dans pip, mettez-le à jour :
```bash pip install --upgrade pip pip-audit # Devrait être propre maintenant ```
</Aside>
<Aside type="note" title="Dépendances dev">
Le fichier `requirements-dev.txt` contient `ruff`, `pytest`, `bandit`, `pip-audit` — tous les outils nécessaires pour les tests locaux.
</Aside>
2. **Tester localement avant de push** :
```bash # Formatter le code automatiquement ruff format src/ tests/
# Vérifier le linting ruff check src/ tests/
# Lancer les tests pytest tests/ -v
# Vérifier les vulnérabilités des dépendances pip-audit
# Scanner le code avec Bandit bandit -r src/ -c pyproject.toml ```
<Aside type="caution" title="Corriger les erreurs avant de continuer">
Si `ruff check` ou `pytest` échouent, corrigez les problèmes avant de commiter. La CI rejettera la PR sinon.
**Ordre recommandé :**
1. `ruff format` → formatter automatiquement 2. `ruff check --fix` → corriger les problèmes auto-fixables 3. Corriger manuellement les erreurs restantes 4. Relancer `ruff check` → doit être ✓ propre
```bash ruff check src/ tests/ --fix ruff format src/ tests/ ```
</Aside>
3. **Commiter tous les fichiers dans la branche `setup`** :
```bash # Vérifiez que vous êtes sur la branche setup git 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 ```
<Aside type="danger" title="Si .venv/ est tracké">
Si `git status` montre `.venv/` dans les fichiers à commiter :
```bash # Supprimer .venv/ du staging git reset .venv/
# Vérifier que .gitignore contient .venv/ echo ".venv/" >> .gitignore
# Re-commiter git add .gitignore git commit -m "feat: initial secure pipeline setup" ```
**Pourquoi c'est critique ?** Commiter `.venv/` fait **échouer 3 checks Scorecard** (Binary-Artifacts, Pinned-Dependencies, SAST) et fait chuter votre score de ~9/10 à ~7/10.
</Aside>
4. **Créer une Pull Request avec gh** :
```bash 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 ```
5. **Suivre l'exécution de la CI** :
```bash # Ouvrir la PR dans le navigateur pour voir les checks gh pr view --web
# Ou suivre le statut en CLI gh pr checks --watch ```
6. **Merger la PR** (une fois les checks verts) :
```bash gh pr merge --squash --delete-branch ```
<Aside type="caution" title="Erreur : At least 1 approving review is required">
Si vous obtenez cette erreur, c'est que vous êtes **seul sur le projet** et ne pouvez pas vous auto-approuver.
**Solution rapide** :
```bash gh pr merge --admin --squash --delete-branch ```
Le flag `--admin` bypass les protections. Vous pouvez aussi modifier temporairement le ruleset pour mettre **0** approbations requises.
</Aside>
<Aside type="caution" title="Erreur : Commits must have verified signatures">
Ce problème vient de Gitsign : les commits sont signés (vérifiables via Rekor) mais GitHub les affiche comme **"Unverified"** car GitHub ne reconnaît que GPG/SSH, pas Sigstore.
**Solutions** :
**Option 1 : Bypass admin** (recommandé pour projets solo)
```bash gh pr merge --admin --squash --delete-branch ```
**Option 2 : Configurer le bypass permanent**
1. GitHub → Settings → Rules → Rulesets → `main-protection` 2. Section **"Bypass list"** → Add bypass 3. Sélectionnez **"Repository admin"** ou votre username 4. Save changes
Désormais vos PRs pourront être mergées même avec commits "Unverified".
**Option 3 : Désactiver la vérification de signature**
1. GitHub → Settings → Rules → Rulesets → `main-protection` 2. Décochez **"Require signed commits"** 3. Save changes
**Note** : Vos commits **sont** signés et sécurisés (vérifiables avec `gitsign verify`), c'est juste que GitHub ne les reconnaît pas encore comme "Verified". C'est une limitation actuelle de GitHub vis-à-vis de Sigstore.
</Aside>
7. **Mettre à jour votre copie locale** :
```bash git checkout main git reset --hard origin/main ```
<Aside type="note" title="Pourquoi reset au lieu de pull ?">
Avec **squash merge**, tous vos commits locaux ont été écrasés en un seul commit sur `main`. Un `git pull` classique échouerait car les historiques ont divergé. Le `reset --hard` force la synchronisation avec origin/main.
</Aside>
8. **Créer le premier tag** :
```bash git tag v1.0.0 git push origin v1.0.0 ```
9. **Vérifier le workflow Release** :
```bash # Suivre l'exécution du workflow Release gh run watch ```
10. **Vérifier Scorecard** : Après quelques minutes, votre score apparaît sur [scorecard.dev](https://scorecard.dev)
</Steps>
<Aside type="tip" title="Pourquoi cette méthode ?">
En passant par une PR dès le premier déploiement, vous :
- Validez que vos workflows fonctionnent correctement- Respectez le processus que vous appliquerez pour tous les futurs changements- Confirmez que les rulesets sont bien configurés
C'est la méthode "dogfooding" — vous utilisez vos propres outils.
</Aside>
### 5.1 Débugger un score Scorecard bas
Si votre score Scorecard est inférieur à 8/10, exécutez Scorecard localementpour identifier les problèmes :
```bash# Installer Scorecard CLIgo install github.com/ossf/scorecard/v5/cmd/scorecard@latest
# Scanner votre reposcorecard --repo=github.com/VOTRE_USER/secure-python-pipeline --show-details```
#### 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** :
```yaml# ❌ 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: read```
**Fixer Binary-Artifacts / Pinned-Dependencies / SAST** :
Ces erreurs viennent du même problème : `.venv/` ne doit **jamais** êtrecommitté.
```bash# 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-details```
**Exemple de .gitignore complet** :
```text# 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
Maintenant que l'image est publiée, vérifions que tout est correct.
### 6.1 Vérifier la signature Cosign
La **signature Cosign** prouve que l'image vient bien de votre workflow derelease. La commande `cosign verify` contrôle le **certificat keyless** contrel'**identité GitHub** attendue : si l'image a été substituée, la vérificationéchoue.
```bash# 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/..."},...}]```
<Aside type="caution" title="Erreur : no signatures found">
Si vous obtenez `Error: no signatures found`, cela signifie que l'image**n'a pas été signée**. Causes possibles :
**1. Le workflow Release n'a pas été déclenché**
```bash# Vérifier les runs du workflowgh run list --workflow=release.yml --limit 5
# Si aucun run, vérifier que le tag commence par 'v'git tag # Doit afficher v1.0.0, pas 1.0.0```
**2. Le workflow a échoué**
```bash# Voir les logs du dernier rungh run view --log
# Chercher les erreurs dans la step "Sign image"```
**3. L'image n'existe pas**
```bash# Vérifier que l'image a été pousséecrane ls ${IMAGE}
# Ou essayer de la pulldocker pull ${IMAGE}:${TAG}```
**4. Mauvais tag/digest**
```bash# Lister tous les tags disponiblescrane ls ${IMAGE}
# Essayer avec le digestDIGEST=$(crane digest ${IMAGE}:${TAG})cosign verify \ --certificate-identity-regexp="https://github.com/VOTRE_USER/secure-python-pipeline" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ ${IMAGE}@${DIGEST}```
**Solution : Re-déclencher le workflow**
```bash# Supprimer et recréer le taggit tag -d v1.0.0git push --delete origin v1.0.0git tag v1.0.0git push origin v1.0.0
# Suivre l'exécutiongh run watch```
</Aside>
### 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.
```bash# Récupérer le digestDIGEST=$(crane digest ${IMAGE}:${TAG})
# Vérifier l'attestation SLSAgh attestation verify oci://${IMAGE}@${DIGEST} \ --owner VOTRE_USER```
**Sortie attendue :**
```✓ Verification succeeded!
sha256:abc123... was attested by a]Sigstore entry with ID 12345678```
### 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.
```bash# 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
Le scan exécuté en CI peut dater de plusieurs heures. Relancer **Trivy** enlocal donne l'état des **vulnérabilités connues** au moment précis où vouscontrôlez l'image, juste avant un déploiement.
```bash# 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
Vous avez maintenant des SBOM et des attestations pour chaque image. Mais sivous gérez 50 services, comment répondre rapidement à : **"Quels servicesutilisent la dépendance vulnérable CVE-2024-XXXX ?"**
[GUAC](/docs/securiser/supply-chain/guac/) (Graph for Understanding ArtifactComposition) agrège tous vos SBOM et attestations dans un **graphe deconnaissances** interrogeable. C'est le centre de contrôle de votre supplychain.
### 7.1 Déployer GUAC localement
Pour ce tutoriel, nous déployons GUAC en local. En production, vous ledéploierez sur un serveur dédié ou dans Kubernetes.
```bash# 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 ps```
**Services 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
Ajoutez ce job dans `.github/workflows/release.yml` après le job `sbom` :
```yaml # =========================================================================== # 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"```
<Aside type="note" title="GUAC hébergé">
Pour que ce job fonctionne en CI, vous devez avoir une instance GUAC accessibledepuis GitHub Actions. Définissez `GUAC_API_URL` dans les variables du dépôt(Settings → Secrets and variables → Variables).
En attendant, vous pouvez exécuter l'ingestion manuellement depuis votre poste.
</Aside>
### 7.3 Ingérer manuellement les artefacts
Si vous n'avez pas de GUAC hébergé, ingérez manuellement après chaque release :
```bash# 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
**Via l'interface web** : Ouvrez http://localhost:3000 et explorez le graphe.
**Via GraphQL** : Trouvez tous les packages vulnérables dans votre image :
```bashcurl -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 :**
```json{"package": "requests", "cve": "CVE-2023-32681"}{"package": "starlette", "cve": "CVE-2024-12345"}```
### 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` |
<Aside type="tip" title="Alertes automatiques">
Combiné avec le certifier OSV de GUAC, vous pouvez créer des alertesautomatiques quand une nouvelle CVE affecte un de vos composants. GUAC surveilleen continu et enrichit le graphe.
</Aside>
## Étape 8 : (Optionnel) Ajouter le fuzzing
Le **fuzzing** teste votre application avec des entrées aléatoires pourdétecter des bugs inattendus (crashes, comportements anormaux). C'est**optionnel** pour un projet simple, mais améliore votre score Scorecard.
### 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)
[Atheris](https://github.com/google/atheris) est un fuzzer Python basé surlibFuzzer.
**`tests/fuzz_main.py`** :
```python"""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`** :
```textatheris>=2.3.0```
**Ajouter à `.github/workflows/ci.yml`** :
```yaml # =========================================================================== # 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=60```
**Tester localement** :
```bash# Installer atherispip install atheris
# Lancer le fuzzing pendant 10 secondespython tests/fuzz_main.py -max_total_time=10```
**Sortie 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)
Pour les projets open source largement utilisés, vous pouvez rejoindre[OSS-Fuzz](https://github.com/google/oss-fuzz) — le fuzzing continu de Google :
1. Suivre le [guide d'intégration](https://google.github.io/oss-fuzz/getting-started/new-project-guide/)2. Google exécute le fuzzing 24/7 sur vos commits3. 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é
<Aside type="note" title="Impact sur Scorecard">
Le check **Fuzzing** passe à 10/10 si :
- Votre repo est sur OSS-Fuzz, **ou**- Vous avez un workflow CI qui exécute du fuzzing, **ou**- Vous mentionnez le fuzzing dans `SECURITY.md`
Pour un projet simple, **ne pas avoir de fuzzing n'est pas bloquant**. Maispour les projets critiques (parseurs, crypto, API publiques), c'est**fortement recommandé**.
</Aside>
## É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 ?
| 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
```bash# Installation via pippip install in-toto
# Vérifier l'installationin-toto-run --versionin-toto-verify --version```
### 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` :
```json title="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": []}```
<Aside type="note" title="Clés et signatures">
Dans cet exemple simplifié, les `pubkeys` sont vides car nous utilisons GitHub Actions comme source de confiance. En production, vous signeriez le layout avec une clé privée et chaque step avec la clé du runner.
</Aside>
### 9.4 Générer des links dans le workflow
Modifiez votre workflow CI pour générer un link à chaque étape :
```yaml title=".github/workflows/ci.yml"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.link```
### 9.5 Vérifier la chaîne complète
Après le build, vérifiez que tous les links correspondent au layout :
```bash# 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
Les attestations in-toto sont compatibles avec l'écosystème Sigstore :
```bash# 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/query```
<Aside type="tip" title="Quand utiliser in-toto complet ?">
- **SLSA seul** : suffisant pour la plupart des projets, prouve la provenance du build- **in-toto complet** : nécessaire si vous devez prouver que des étapes spécifiques (review humaine, scan de sécurité) ont eu lieu
Pour ce guide, SLSA couvre les besoins courants. Explorez in-toto complet si vous avez des exigences de compliance strictes.
</Aside>
<LinkCard title="Guide complet in-toto" description="Comprendre layouts, links et vérification en profondeur" href="/docs/securiser/supply-chain/in-toto/"/>
## Dépannage
Voici les **erreurs les plus fréquentes** rencontrées en construisant cepipeline, 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 |
<Aside type="tip" title="Débugger les permissions">
Les **permissions effectives** du `GITHUB_TOKEN` sont affichées par GitHublui-même : ouvrez l'exécution du workflow, dépliez le step **« Set up job »**et cherchez le bloc `GITHUB_TOKEN Permissions`. Il liste exactement lesportées accordées au job.
N'écrivez **jamais** `echo` sur `github.token` ni `toJSON(secrets)` : celarevient à écrire un secret dans les logs.
</Aside>
## Résumé des protections
Le pipeline empile **plusieurs couches** de protection, chacune fermant unevoie 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
1. **Branch protection** est le premier rempart : pas de commit direct sur main2. **Actions épinglées par SHA** : les tags peuvent être modifiés, pas les SHA3. **Hash pinning** pour les dépendances : garantit l'intégrité même si PyPI est compromis4. **SLSA attestation** prouve que le build vient de votre CI, pas d'un laptop compromis5. **Signature Cosign** permet à vos consommateurs de vérifier l'authenticité6. **SBOM** documente exactement ce qui est dans l'image pour répondre aux audits7. **GUAC** connecte tous vos SBOM pour répondre rapidement aux CVE 0-day8. **Scorecard** vous donne une note et des axes d'amélioration concrets9. **in-toto** garantit que le pipeline complet a été respecté (lint → test → audit → build)
## Prochaines étapes
<CardGrid>
<LinkCard title="Checklist sécurité GitHub Actions" description="Repasser chaque point de durcissement en revue" href="/docs/pipeline-cicd/github/securite/checklist/"/>
<LinkCard title="GUAC : graphe de dépendances" description="Analyser les relations entre vos composants" href="/docs/securiser/supply-chain/guac/"/>
<LinkCard title="in-toto : attestations complètes" description="Définir des layouts et vérifier toute la chaîne de build" href="/docs/securiser/supply-chain/in-toto/"/>
<LinkCard title="VEX : gérer les faux positifs" description="Documenter les vulnérabilités non applicables" href="/docs/securiser/supply-chain/vex/"/>
</CardGrid>
## Ressources externes
Pour approfondir chaque maillon de la chaîne, ces **références officielles**complètent le guide.
- [SLSA GitHub Generator](https://github.com/slsa-framework/slsa-github-generator)- [GitHub Attestations documentation](https://docs.github.com/en/actions/security-guides/using-artifact-attestations)- [Cosign documentation](https://docs.sigstore.dev/cosign/signing/signing_with_containers/)- [OpenSSF Scorecard Checks](https://scorecard.dev/checks)- [pip-audit documentation](https://github.com/pypa/pip-audit)