
Les quatre guides précédents ont construit un pipeline qui coche déjà douze contrôles garantis d'OpenSSF Scorecard à 10, plus deux (CI-Tests, Vulnerabilities) qui tiennent 10 selon l'état courant du dépôt. Ce guide ferme la boucle : il décortique chaque contrôle depuis son code source, explique comment sa note est calculée, montre le code exact qui le satisfait dans notre dépôt, et sépare les leviers actionnables des plafonds structurels qu'aucune configuration ne débloque. Public visé : un mainteneur avancé qui veut la meilleure note sans tricher. À la fin, vous saurez lire un rapport Scorecard ligne par ligne et agir sur ce qui dépend vraiment de vous.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Faire tourner Scorecard en CI et en local, puis lire son rapport
- Comprendre le calcul de moyenne pondérée et le poids de chaque contrôle
- Reproduire le code qui satisfait Signed-Releases, Fuzzing et Pinned-Dependencies
- Distinguer les contrôles acquis, les leviers à activer et le plafond Maintained
Prérequis
Section intitulée « Prérequis »Le pipeline complet est en place : CI durci,
build vérifiable et
protection de branche.
Le dépôt est public (Scorecard n'évalue pleinement que les dépôts publics) et
la CLI gh est authentifiée. Tout le code de ce guide provient du dépôt de
référence, public et consultable :
github.com/stephrobert/secure-python-pipeline.
Comment Scorecard calcule un score
Section intitulée « Comment Scorecard calcule un score »Le score global est une moyenne pondérée des contrôles, ramenée sur 10. Chaque contrôle porte un niveau de risque qui fixe son poids dans la moyenne :
| Niveau de risque | Poids | Logique |
|---|---|---|
| Critical | 10 | Une faille directement exploitable |
| High | 7,5 | Un durcissement essentiel de la chaîne |
| Medium | 5 | Une bonne pratique de robustesse |
| Low | 2,5 | Un signal de qualité |
Cette pondération a une conséquence pratique : un point gagné sur un contrôle High vaut trois fois un point gagné sur un Low. Quand on optimise, on regarde d'abord les contrôles à fort poids encore en dessous de 10.
Deux subtilités de calcul reviennent partout. Un contrôle inconclusif (le token n'a pas pu lire la donnée nécessaire) n'est pas compté dans la moyenne : le rendre lisible mais mal réglé peut donc faire baisser la note au lieu de la monter. Et chaque contrôle est soit binaire (0 ou 10, présence d'un fichier), soit proportionnel (une fraction, souvent avec une division entière qui pénalise durement le moindre écart).
Faire tourner Scorecard
Section intitulée « Faire tourner Scorecard »Deux façons de l'exécuter : un workflow qui publie le score en continu, et une commande locale pour itérer sans attendre.
Le workflow ci-dessous s'exécute au push sur main, chaque semaine et à chaque
changement de règle de branche. Il publie le résultat (badge et API OpenSSF) et
téléverse un SARIF visible dans l'onglet Security.
name: Scorecard
on: branch_protection_rule: push: branches: [main] schedule: - cron: "0 7 * * 1"
permissions: {}
jobs: analysis: name: OpenSSF Scorecard runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: security-events: write # upload du SARIF id-token: write # publication signée des résultats contents: read actions: read steps: - name: Harden runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Analyse Scorecard uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif publish_results: true - name: Publier le SARIF uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: sarif_file: results.sarifPour itérer sans attendre le prochain run, on lance Scorecard en local via son image officielle, avec un jeton en lecture seule :
export GITHUB_AUTH_TOKEN=$(gh auth token)
docker run -e GITHUB_AUTH_TOKEN \ gcr.io/openssf/scorecard:stable \ --repo=github.com/stephrobert/secure-python-pipelineLa sortie liste chaque contrôle avec sa note et le score agrégé. C'est cette vue qu'on lit ligne par ligne dans la suite du guide :
Aggregate score: 8.0 / 10
Check scores:| SCORE | NAME ||----------|-------------------------|| 10 / 10 | Token-Permissions || 10 / 10 | Pinned-Dependencies || 10 / 10 | Signed-Releases || 10 / 10 | SAST || 0 / 10 | Maintained || ? / 10 | Branch-Protection |Le tableau de bord des dix-neuf contrôles
Section intitulée « Le tableau de bord des dix-neuf contrôles »Voici la vue d'ensemble. La colonne Notre note correspond à l'état du dépôt
secure-python-pipeline construit dans cette série. La colonne Nature
distingue ce qui est acquis par le pipeline, ce qui reste un levier à
activer, et le plafond structurel.
| Contrôle | Risque | Ce qu'il mesure | Notre note | Nature |
|---|---|---|---|---|
| Token-Permissions | High | Moindre privilège du GITHUB_TOKEN | 10 | Acquis |
| Dangerous-Workflow | Critical | Aucun pattern de workflow dangereux | 10 | Acquis |
| Pinned-Dependencies | Medium | Dépendances figées par SHA/digest/hash | 10 | Acquis |
| SAST | Medium | Analyse statique sur chaque PR | 10 | Acquis |
| Binary-Artifacts | High | Aucun binaire exécutable commité | 10 | Acquis |
| Dependency-Update-Tool | High | Dependabot ou Renovate présent | 10 | Acquis |
| License | Low | Fichier LICENSE reconnu SPDX | 10 | Acquis |
| Packaging | Medium | Publication d'artefacts par workflow | 10 | Acquis |
| Webhooks | Critical | Webhooks avec secret (ou aucun) | 10 | Acquis |
| Security-Policy | Medium | SECURITY.md complet | 10 | Acquis (levier) |
| Signed-Releases | High | Provenance/signature sur les releases | 10 | Acquis (levier) |
| Fuzzing | Medium | Intégration de fuzzing | 10 | Acquis (levier) |
| CI-Tests | Low | Tests CI verts sur les PR récentes | 10* | Acquis |
| Vulnerabilities | High | Aucun CVE OSV ouvert | 10* | Acquis |
| Code-Review | High | Changements approuvés par un tiers | variable | Levier |
| Contributors | Low | Entités (Company) distinctes | variable | Levier |
| Branch-Protection | High | Règles de protection de branche | paliers | Levier |
| CII-Best-Practices | Low | Badge bestpractices.dev | 0 | Levier |
| Maintained | High | Activité récente / ancienneté | 0 | Plafond |
Les notes marquées * dépendent d'un état qui peut évoluer sans commit : une PR
non testée casse le 10 de CI-Tests, un nouveau CVE fait chuter Vulnerabilities.
Les contrôles que le pipeline coche déjà
Section intitulée « Les contrôles que le pipeline coche déjà »La majorité des contrôles sont satisfaits par le simple fait d'avoir suivi les guides précédents. Comprendre pourquoi évite de croire qu'il faut en faire plus.
Les acquis « gratuits » d'un pipeline durci
Section intitulée « Les acquis « gratuits » d'un pipeline durci »Ces contrôles ne demandent aucun travail dédié au scoring : un pipeline propre les coche par construction.
- Token-Permissions (High) mesure le moindre privilège du
GITHUB_TOKEN. Nos workflows déclarentpermissions: {}au niveau global et des permissions minimales par job. Le contrôle part de 10 et soustrait des points pour chaque écriture globale : sans aucune, on reste à 10. - Dangerous-Workflow (Critical) est binaire : 10 ou 0. Il cherche deux
patterns, l'
untrusted checkouten contexte privilégié (pull_request_targetsuivi d'un checkout de la PR) et l'injection de script. Aucun de nos sept workflows ne les présente, donc 10. - Binary-Artifacts (High) vérifie qu'aucun binaire exécutable n'est
commité. Notre code est du Python source, l'image se construit depuis le
Dockerfile: la sonde ne trouve aucun artefact, donc 10. - License (Low) est additif : le fichier
LICENSEà la racine vaut +9, et un identifiant SPDX reconnu par la FSF ou l'OSI (ici MIT) vaut le +1 final. - Packaging (Medium) cherche un workflow de publication reconnu avec au
moins un run réussi. Notre
release.ymlpousse une image sur GHCR viadocker/build-push-action, et la releasev1.0.0prouve un run réussi. - Webhooks (Critical) exige que chaque webhook porte un secret. Le dépôt n'en a aucun : le contrôle est NotApplicable et retourne 10 par défaut. Ne pas confondre ce 10 « par absence » avec un 10 mérité.
- CI-Tests (Low) mesure la part des PR mergées récentes portant un check de
test en succès. Notre
ci.ymllancepytestsur chaquepull_request. Le 10 exige que 100 % des trente dernières PR soient couvertes. - Vulnerabilities (High) interroge OSV.dev et retire un point par CVE
ouvert. On reste à 10 tant qu'aucune vulnérabilité connue ne touche nos
dépendances, d'où la veille active
pip-audit,Trivyet Dependabot.
Dependency-Update-Tool (High) est lui aussi binaire : la seule présence
de .github/dependabot.yml au bon chemin suffit, le contrôle ne juge pas la
configuration. Le dépôt en profite pour ajouter une quarantaine
anti-supply-chain (le bloc cooldown, qui interdit d'adopter une version le jour
de sa sortie) : elle ne rapporte aucun point mais durcit pour de vrai. Sa
configuration complète est détaillée dans le
bootstrap.
Pinned-Dependencies : figer par hash
Section intitulée « Pinned-Dependencies : figer par hash »Pinned-Dependencies (Medium) exige que tous les écosystèmes soient figés.
Un pip install fastapi==0.139.1 fixe la version mais pas le hash : Scorecard
le compte comme non épinglé. Le point clé : == ne suffit pas, il faut un
lockfile installé en --require-hashes, et tous les écosystèmes doivent
être figés, les actions par SHA de commit, l'image de base par digest
@sha256 et Python par hash. La mécanique complète du .in compilé avec
--generate-hashes est décrite dans le
bootstrap.
SAST : CodeQL sur les pull requests
Section intitulée « SAST : CodeQL sur les pull requests »SAST (Medium) veut une analyse statique sur les PR. Notre codeql.yml
exécute github/codeql-action/analyze sur pull_request : chaque PR mergée porte
le check github-code-scanning. Détail de calcul important, CodeQL déclenché en
cron seul plafonnerait à 7 ; c'est le déclenchement sur PR qui monte à 10.
Security-Policy : le SECURITY.md à paliers
Section intitulée « Security-Policy : le SECURITY.md à paliers »Security-Policy (Medium) est additif à paliers. La présence d'un
SECURITY.md débloque le contrôle, puis le score additionne : +6 pour un lien
ou un email, +3 pour du texte réel (pas un gabarit vide), +1 pour au moins
deux marqueurs de divulgation (les racines « vuln » et « disclos », plus des
délais chiffrés). Notre fichier porte l'email security@, l'URL d'advisory,
une vraie prose et les délais (48 h, 90 jours) : 10.
Signed-Releases : la provenance en asset
Section intitulée « Signed-Releases : la provenance en asset »Signed-Releases (High) cherche, sur les cinq dernières releases, un asset
de provenance ou de signature. Point clé découvert dans le code : le contrôle
regarde un fichier *.intoto.jsonl attaché à la release, pas l'attestation OCI
de l'image, deux artefacts distincts. Une provenance vaut 10 points pour cette
release, et le score est la moyenne plancher sur les releases : avec une seule
release provenue, floor(10/1) = 10. Le step release.yml qui copie le bundle en
provenance.intoto.jsonl et l'attache à la release est détaillé dans
Build vérifiable.
Fuzzing : le harnais Atheris
Section intitulée « Fuzzing : le harnais Atheris »Fuzzing (Medium) est binaire et d'une simplicité déroutante : pour un
projet Python, la sonde passe à True dès qu'un fichier contient import atheris. Pour rester honnête plutôt que de poser un fichier vide, notre
harnais envoie de vraies entrées aléatoires à l'API et échoue si le serveur
renvoie un 5xx :
import sysimport atheris
# On n'instrumente PAS les imports : pydantic-core est une extension compilée# (Rust) dont le loader fait segfault atheris.instrument_imports().from fastapi.testclient import TestClient # noqa: E402from app.main import app # noqa: E402
client = TestClient(app)
def test_one_input(data: bytes) -> None: fdp = atheris.FuzzedDataProvider(data) path = fdp.ConsumeUnicodeNoSurrogates(64) try: response = client.get("/" + path) except Exception: # noqa: BLE001 return # Un 5xx sur une entrée malformée signale un bug à corriger. assert response.status_code < 500
def main() -> None: atheris.Setup(sys.argv, test_one_input) atheris.Fuzz()
if __name__ == "__main__": main()Le workflow fuzz.yml l'exécute avec un budget borné, ce qui rend le contrôle
honnête sans bloquer la CI trop longtemps :
# .github/workflows/fuzz.yml (step d'exécution) - name: Run fuzzing (budget limité) run: python fuzz/fuzz_api.py -runs=20000 -max_total_time=120Le score serait le même avec un fichier factice ; la différence est éthique, pas mécanique.
Les leviers qu'il reste à activer
Section intitulée « Les leviers qu'il reste à activer »Après les guides précédents, quatre contrôles dépendent encore d'un effort ciblé.
Branch-Protection : monter les paliers
Section intitulée « Branch-Protection : monter les paliers »Branch-Protection (High) évalue la branche par défaut par paliers
cumulatifs : chaque palier doit être plein pour débloquer le suivant, du socle
(deletion et non_fast_forward) jusqu'au zéro bypass admin. Le contrôle lit
les règles via le token, donc un ruleset actif et lisible est un prérequis
du scoring. Le détail des cinq paliers, le descripteur ruleset.json et la
commande gh api qui le pose sont traités dans Protection et
gouvernance.
Les paliers hauts exigent une équipe : deux approbations supposent deux relecteurs. Le dernier palier (zéro bypass admin) reste discutable sur un dépôt solo, où l'auteur a besoin de pouvoir débloquer une situation. On monte aussi haut que l'organisation le permet, sans se mentir sur ce qui est tenable.
Code-Review : la discipline de la PR
Section intitulée « Code-Review : la discipline de la PR »Code-Review (High) calcule la proportion des trente derniers changesets
approuvés par un compte différent de l'auteur, les changesets de bots étant
exclus du dénominateur. Concrètement, il faut faire passer tout changement par
une PR approuvée par un tiers et interdire les push directs sur main. Nos
deux collaborateurs rendent cette approbation croisée possible ; le score monte
avec le temps et la discipline, pas avec un réglage unique.
Contributors : plusieurs organisations
Section intitulée « Contributors : plusieurs organisations »Contributors (Low) compte le nombre d'entités distinctes parmi les contributeurs ayant au moins cinq contributions, une entité étant une organisation publique ou le champ Company du profil. Trois entités distinctes suffisent pour 10 (le dénominateur est plafonné à 3, avec division entière). Il faut donc que trois comptes renseignent un Company différent et contribuent réellement. C'est un levier social, pas technique.
CII-Best-Practices : le badge
Section intitulée « CII-Best-Practices : le badge »CII-Best-Practices (Low) interroge l'API de bestpractices.dev pour l'URL du
dépôt. Les paliers donnent des points : In Progress 2, Passing 5,
Silver 7, Gold 10. C'est le meilleur rapport effort/gain restant.
-
S'inscrire sur
bestpractices.dev, se connecter avec GitHub, ajouter le projet par son URL. L'inscription seule donne déjà In Progress (2 points). -
Remplir le questionnaire Passing (environ 66 critères). Un dépôt qui suit cette série remplit déjà l'essentiel : licence, README, HTTPS, versioning,
SECURITY.md, tests en CI, analyse statique, livraison signée. -
Répondre honnêtement : chaque critère se coche Met, Unmet ou N/A avec justification. Les critères de cryptographie sont N/A si le projet n'implémente pas de crypto propre.
Le plafond structurel : Maintained
Section intitulée « Le plafond structurel : Maintained »Maintained (High) est le seul contrôle qu'aucune configuration ne débloque
à court terme. Le code de Scorecard court-circuite le calcul pour un dépôt
récent : la constante lookBackDays = 90 et un test recentlyCreated forcent
le score minimal tant que le dépôt a moins de 90 jours, quelle que soit
l'activité. C'est du temps, pas de la configuration. Passé ce délai, le
contrôle mesure les commits et issues sur la fenêtre glissante ; il faut alors une
activité régulière pour tenir la note.
Deux autres contrôles ont, en plus de leur levier, une part de temps incompressible : Code-Review et Contributors montent avec l'historique de PR revues et l'accumulation de contributions. On les active, mais leur plafond réel n'apparaît qu'avec les mois.
Ne pas gamer le score
Section intitulée « Ne pas gamer le score »Le score est un moyen, pas une fin. Ajouter un outil uniquement pour cocher
un contrôle est du gaming : si un contrôle est déjà satisfait par le bon outil
(CodeQL pour l'analyse statique), on n'en empile pas un second pour « verrouiller »
la note. De même, on ne déclare pas Met un critère bestpractices.dev qu'on ne
remplit pas vraiment, et on ne pose pas un import atheris dans un fichier vide
pour simuler du fuzzing. Un score obtenu honnêtement reflète une posture de
sécurité réelle ; un score gonflé ne trompe que soi-même, et s'effondre au
premier audit sérieux.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »- Le score est une moyenne pondérée : Critical vaut 10, High 7,5, Medium 5, Low 2,5. Un point sur un High pèse trois fois un point sur un Low.
- Un contrôle inconclusif rendu lisible mais mal réglé fait baisser la note : ne rendre lisible que ce qui est correctement configuré.
- Douze contrôles sont garantis à 10 par construction, plus deux (CI-Tests, Vulnerabilities) selon l'état courant ; comprendre leur calcul évite d'en faire trop.
- Leviers restants : Branch-Protection (paliers), Code-Review (PR croisées), Contributors (trois entités), CII-Best-Practices (badge Passing).
- Plafond structurel : Maintained (constante des 90 jours, prouvée par le code), plus la part temps de Code-Review et Contributors.
- Le score est un moyen : on ne le game pas, on durcit pour de vrai.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Retour au hub du lab : la série complète et le dépôt de référence.
- Checklist sécurité : le récapitulatif des règles à vérifier.
- Attaques supply chain : ce contre quoi tout ce durcissement protège.