Aller au contenu
CI/CD & Automatisation medium

Pipeline CI/CD sécurisé : guide pratique Python + GitHub Actions

77 min de lecture

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
  • 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

Une application Python minimaliste (API FastAPI) avec un pipeline qui :

  1. Analyse le code : linting, tests, SAST avec attestations in-toto
  2. Audite les dépendances : vulnérabilités, intégrité (hash pinning)
  3. Construit l'image Docker avec attestation SLSA L3
  4. Génère le SBOM (liste des composants) en SPDX et CycloneDX
  5. Signe l'image avec Cosign (keyless via Sigstore)
  6. Publie sur GHCR avec provenance vérifiable
  7. Ingère dans GUAC pour visibilité globale sur les dépendances
  8. É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 ✓ │ │
│ └────────────────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

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émentVersion minimaleVérificationInstallation
Git2.34+git --versionGuide Git
Python3.11+python --versionGuide pyenv
Docker24+docker --versionGuide Docker
Gitsign0.10+gitsign versionGuide Gitsign
Cosign2.0+cosign versionGuide Cosign
Syft1.0+syft versionGuide Syft
Trivy0.50+trivy versionGuide Trivy
slsa-verifier2.0+slsa-verifier versionGitHub

La première étape est de créer un dépôt GitHub correctement configuré. Les choix faits ici impactent directement votre score Scorecard.

  1. Créer le dépôt : Allez sur github.com/new

  2. Configurer les paramètres de base :

    ParamètreValeur recommandéePourquoi
    Repository namesecure-python-pipelineNom explicite
    Description"API Python avec pipeline CI/CD sécurisé"Aide au référencement
    VisibilityPublicScorecard fonctionne mieux sur les dépôts publics
    Add README✅ OuiPoint d'entrée pour les contributeurs
    Add .gitignorePythonExclut __pycache__, .venv, .pytest_cache
    LicenseMIT ou Apache 2.0Clarté juridique, check License Scorecard
  3. Cliquer sur "Create repository"

Cloner le dépôt et créer une branche de travail :

Fenêtre de terminal
# Cloner le dépôt (remplacez VOTRE_USER par votre username GitHub)
git clone https://github.com/VOTRE_USER/secure-python-pipeline.git
cd 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 place
ls -la
# Vous devriez voir : .git/ .gitignore LICENSE README.md

Configurer Git localement (si pas déjà fait) :

Fenêtre de terminal
# Configurer votre identité (utilisée pour les commits)
git config user.name "Votre Nom"
git config user.email "votre.email@example.com"

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 :

Fenêtre de terminal
# macOS
brew 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_amd64
sudo mv gitsign_*_linux_amd64 /usr/local/bin/gitsign
# Vérifier l'installation
gitsign version

Configurer Git pour utiliser Gitsign (dans ce projet uniquement) :

Fenêtre de terminal
# Activer la signature automatique des commits
git config --local commit.gpgsign true
# Utiliser le format x509 (requis pour Gitsign)
git config --local gpg.format x509
# Spécifier Gitsign comme programme de signature
git config --local gpg.x509.program gitsign

Tester la signature :

Fenêtre de terminal
# Créer un fichier de test
echo "test" > test.txt
git add test.txt
git 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 :

Fenêtre de terminal
git log --show-signature -1

Sortie attendue :

commit abc123...
gitsign: Signature made Mon Dec 30 10:00:00 2024 CET
gitsign: Good signature from "votre-email@example.com"
gitsign: WARNING: no matching key found in keyring
This is expected for keyless signing

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.

  1. 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 RulesRulesets
  2. Créer un nouveau ruleset :

    • Cliquez sur New rulesetNew branch ruleset
    • Ruleset name : main-protection (nom descriptif)
    • Enforcement status : Active (appliqué immédiatement)
  3. Configurer le ciblage (Target branches) :

    • Cliquez sur Add targetInclude default branch
    • Cela cible automatiquement main (ou master selon votre config)
  4. Configurer les règles de base :

    Dans la section Rules, activez ces protections :

    RègleActionEffetCheck Scorecard
    Restrict deletions✅ CocherEmpêche la suppression de la branche
    Require linear history✅ CocherForce squash ou rebase (pas de merge commits)
    Block force pushes✅ Cocher (défaut)Empêche git push --force
  5. 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 :
    OptionValeurExplication
    Required approvals1Nombre minimum de reviewers (augmentez pour les projets critiques)
    Dismiss stale pull request approvals when new commits are pushedInvalide les approvals si le code change après review
    Require review from Code OwnersForce l'approbation par les CODEOWNERS
    Require approval of the most recent reviewable pushL'auteur du dernier push ne peut pas auto-approuver
    Require conversation resolution before mergingTous les commentaires doivent être résolus
  6. 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)
  7. Configurer "Require signed commits" :

    • ✅ Cocher Require signed commits
    • Seuls les commits signés seront acceptés

    GitHub reconnaît trois types de signatures :

    MéthodeBadge "Verified"Gestion de clés
    GPG✅ NatifClé à créer et protéger
    SSH✅ NatifUtilise votre clé SSH
    Gitsign⚠️ Via RekorAucune (keyless)
  8. 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.

  9. 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) │
└─────────────────────────────────────────────────────────────────────────┘

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)

Scorecard évalue plusieurs critères. Pour atteindre un score élevé (8+/10), vous devez ajouter ces fichiers.

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 Docker
Dockerfile @VOTRE_USER
.dockerignore @VOTRE_USER
# Dépendances : revue obligatoire
requirements*.txt @VOTRE_USER
pyproject.toml @VOTRE_USER

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)

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: false
contact_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"

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 :

# Guide de contribution
Merci de votre intérêt pour ce projet !
## Prérequis
- Python 3.11+
- Docker 24+
- Git
## Installation locale
```bash
git clone https://github.com/VOTRE_USER/secure-python-pipeline.git
cd secure-python-pipeline
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
Fenêtre de terminal
pytest
ruff check src/ tests/
bandit -r src/
  1. Forkez le projet
  2. 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

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 les
contributeurs. Il doit inclure les badges de sécurité.
**`README.md`** :
````markdown
# Secure Python Pipeline
[![CI](https://github.com/VOTRE_USER/secure-python-pipeline/actions/workflows/ci.yml/badge.svg)](https://github.com/VOTRE_USER/secure-python-pipeline/actions/workflows/ci.yml)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/VOTRE_USER/secure-python-pipeline/badge)](https://scorecard.dev/viewer/?uri=github.com/VOTRE_USER/secure-python-pipeline)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/XXXXX/badge)](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 Cosign
cosign 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 SLSA
gh attestation verify oci://ghcr.io/VOTRE_USER/secure-python-pipeline:latest \
--owner VOTRE_USER
  • Python 3.11+
  • Docker 24+
Fenêtre de terminal
git clone
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Fenêtre de terminal
uvicorn app.main:app --reload

L'API est disponible sur http://localhost:8000

Fenêtre de terminal
docker pull ghcr.io/VOTRE_USER/secure-python-pipeline:latest
docker run -p 8000:8000 ghcr.io/VOTRE_USER/secure-python-pipeline:latest
EndpointMéthodeDescription
/GETMessage de bienvenue
/healthGETHealth check pour Kubernetes
Fenêtre de terminal
# Installer les dépendances de dev
pip install -r requirements-dev.txt
# Lancer les tests
pytest
# Linting
ruff check src/ tests/
# Analyse de sécurité
bandit -r src/

MIT

Voir CONTRIBUTING.md

Voir SECURITY.md

#### 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 |
<Aside type="tip" title="Score cible">
Avec tous ces fichiers, vous devriez atteindre un score **8-9/10**. Les checks
Fuzzing 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 votre
environnement virtuel, ce qui fera **échouer 3 checks Scorecard** et fera
chuter 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-tools
pip install pip-tools
# 2. Créer requirements.in (fichier source sans hash)
cat > requirements.in << EOF
fastapi
uvicorn
EOF
# 3. Générer requirements.txt avec TOUTES les dépendances et hashs
pip-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épendances
transitives (anyio, idna, sniffio, typing-extensions, etc.) et **tous** leurs
hashs. 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 pydantic
anyio==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.11
annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc879daf1b616d8de89816493b8fd5c794f47
# via pydantic
anyio==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` sur
votre machine — il contiendra les hashs corrects et complets.
</Aside>
**`requirements-dev.txt`** — Dépendances de développement :
```text
# Outils de qualité
ruff>=0.8.0
bandit>=1.7.0
pip-audit>=2.7.0
# Tests
pytest>=8.0.0
pytest-cov>=6.0.0 # Plugin coverage pour pytest
httpx>=0.27.0 # Pour TestClient FastAPI
# Build
pip-tools>=7.4.0
```
### 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`** :
```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_Store
Thumbs.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-dessus
cat > .gitignore << 'EOF'
.venv/
# ... (copier le contenu complet ci-dessus)
EOF
# Supprimer .venv/ de Git (mais pas du disque)
git rm -r --cached .venv/
# Commiter
git add .gitignore
git 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/` causent
des erreurs `path escapes from parent`, ce qui fait échouer les checks
Binary-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 un
build **reproductible** même si le tag `python:3.11-slim` est republié sur le
registre.
**`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 unbuffered
ENV PYTHONDONTWRITEBYTECODE=1
ENV 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épendances
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# --require-hashes force la vérification des hash
RUN 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-root
RUN groupadd --gid 1000 appgroup && \
useradd --uid 1000 --gid 1000 --shell /bin/false appuser
WORKDIR /app
# Copier le virtualenv depuis le builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copier le code source
COPY --chown=appuser:appgroup src/ ./src/
# Passer en utilisateur non-root
USER 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émarrage
CMD ["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 docs
tests/
docs/
*.md
# CI/CD
.github/
```
## Étape 3 : Configurer Dependabot
[Dependabot](https://docs.github.com/en/code-security/dependabot) crée
automatiquement des PR pour mettre à jour vos dépendances quand des
vulnérabilités sont découvertes.
**`.github/dependabot.yml`** :
```yaml
version: 2
updates:
# 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 Dependabot
gh pr list --author "app/dependabot"
# Merger toutes les PRs Dependabot qui passent la CI
gh 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écifique
gh 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`** :
```yaml
name: Dependabot Auto-merge
on: pull_request
# Aucun droit par défaut : le job demande le strict nécessaire
permissions: {}
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'exigez
pas d'approbation** pour les PRs, ou si vous configurez le bypass pour
Dependabot 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`** :
```yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Aucun droit par défaut : chaque job demande le strict nécessaire
permissions: {}
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**. Le
guide [É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 avec
attestation SLSA, génère le SBOM et signe avec Cosign.
**`.github/workflows/release.yml`** :
```yaml
name: Release
on:
push:
tags:
- "v*"
# Aucun droit par défaut : chaque job demande le strict nécessaire
permissions: {}
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 des
vulnérabilités de sécurité et des erreurs de qualité avec CodeQL.
**`.github/workflows/codeql.yml`** :
```yaml
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écessaire
permissions: {}
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ées
de CodeQL, détectant plus de problèmes que l'analyse standard. Cela augmente
lé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 les
résultats sur [OpenSSF Scorecard](https://scorecard.dev).
**`.github/workflows/scorecard.yml`** :
```yaml
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écessaire
permissions: {}
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`** :
```yaml
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écessaire
permissions: {}
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 ou
compromission des attestations SLSA après publication. C'est une couche de
sé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 commits
directs sur `main` — c'est normal, c'est ce qu'on veut ! Nous allons merger
via 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 localement
pour identifier les problèmes :
```bash
# Installer Scorecard CLI
go install github.com/ossf/scorecard/v5/cmd/scorecard@latest
# Scanner votre repo
scorecard --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ées
permissions: read-all
# ✅ Bon — explicite et minimal
permissions:
contents: read
```
**Fixer Binary-Artifacts / Pinned-Dependencies / SAST** :
Ces erreurs viennent du même problème : `.venv/` ne doit **jamais** être
committé.
```bash
# 1. Ajouter au .gitignore
echo ".venv/" >> .gitignore
# 2. Supprimer du repo
git rm -r --cached .venv/
git commit -m "fix: remove venv from git"
git push
# 3. Re-scanner
scorecard --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_Store
Thumbs.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 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.
```bash
# Définir les variables
export IMAGE=ghcr.io/VOTRE_USER/secure-python-pipeline
export TAG=v1.0.0
# Vérifier la signature
cosign 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 workflow
gh 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 run
gh 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ée
crane ls ${IMAGE}
# Ou essayer de la pull
docker pull ${IMAGE}:${TAG}
```
**4. Mauvais tag/digest**
```bash
# Lister tous les tags disponibles
crane ls ${IMAGE}
# Essayer avec le digest
DIGEST=$(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 tag
git tag -d v1.0.0
git push --delete origin v1.0.0
git tag v1.0.0
git push origin v1.0.0
# Suivre l'exécution
gh 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 digest
DIGEST=$(crane digest ${IMAGE}:${TAG})
# Vérifier l'attestation SLSA
gh 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** 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.
```bash
# Scanner l'image pour vulnérabilités
trivy image ${IMAGE}:${TAG}
# Scanner uniquement les CRITICAL et HIGH
trivy 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 si
vous gérez 50 services, comment répondre rapidement à : **"Quels services
utilisent la dépendance vulnérable CVE-2024-XXXX ?"**
[GUAC](/docs/securiser/supply-chain/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
Pour ce tutoriel, nous déployons GUAC en local. En production, vous le
déploierez sur un serveur dédié ou dans Kubernetes.
```bash
# Cloner le dépôt GUAC
git clone https://github.com/guacsec/guac.git
cd guac
# Démarrer tous les services avec Docker Compose
docker compose up -d
# Vérifier que les services tournent
docker 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 accessible
depuis 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
# Variables
export IMAGE=ghcr.io/VOTRE_USER/secure-python-pipeline
export TAG=v1.0.0
export GUAC_API=http://localhost:8080/query
# Télécharger le SBOM depuis les artifacts GitHub
gh run download --name sbom
# Ingérer les SBOM
guacone 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 :
```bash
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 :**
```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 alertes
automatiques quand une nouvelle CVE affecte un de vos composants. GUAC surveille
en continu et enrichit le graphe.
</Aside>
## É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 ?
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é sur
libFuzzer.
**`tests/fuzz_main.py`** :
```python
"""Fuzzing de l'API avec Atheris."""
import atheris
import 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`** :
```text
atheris>=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 atheris
pip install atheris
# Lancer le fuzzing pendant 10 secondes
python tests/fuzz_main.py -max_total_time=10
```
**Sortie attendue** :
```
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 1234567890
INFO: -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 commits
3. 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**. Mais
pour 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 pip
pip install in-toto
# Vérifier l'installation
in-toto-run --version
in-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 links
gh run download --name link-lint
gh run download --name link-test
gh run download --name link-audit
gh run download --name link-build
# Vérifier contre le layout
in-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-toto
cosign 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-toto
guacone 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 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 |
<Aside type="tip" title="Débugger les permissions">
Les **permissions effectives** du `GITHUB_TOKEN` sont affichées par GitHub
lui-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 les
portées accordées au job.
N'écrivez **jamais** `echo` sur `github.token` ni `toJSON(secrets)` : cela
revient à écrire un secret dans les logs.
</Aside>
## 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
1. **Branch protection** est le premier rempart : pas de commit direct sur main
2. **Actions épinglées par SHA** : les tags peuvent être modifiés, pas les SHA
3. **Hash pinning** pour les dépendances : garantit l'intégrité même si PyPI est compromis
4. **SLSA attestation** prouve que le build vient de votre CI, pas d'un laptop compromis
5. **Signature Cosign** permet à vos consommateurs de vérifier l'authenticité
6. **SBOM** documente exactement ce qui est dans l'image pour répondre aux audits
7. **GUAC** connecte tous vos SBOM pour répondre rapidement aux CVE 0-day
8. **Scorecard** vous donne une note et des axes d'amélioration concrets
9. **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)

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn