Aller au contenu
CI/CD & Automatisation medium

Scoring et durcissement : les 19 contrôles Scorecard

20 min de lecture

logo github

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.

  • 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

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.

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 risquePoidsLogique
Critical10Une faille directement exploitable
High7,5Un durcissement essentiel de la chaîne
Medium5Une bonne pratique de robustesse
Low2,5Un 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).

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.

.github/workflows/scorecard.yml
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.sarif

Pour itérer sans attendre le prochain run, on lance Scorecard en local via son image officielle, avec un jeton en lecture seule :

Fenêtre de terminal
export GITHUB_AUTH_TOKEN=$(gh auth token)
docker run -e GITHUB_AUTH_TOKEN \
gcr.io/openssf/scorecard:stable \
--repo=github.com/stephrobert/secure-python-pipeline

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

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ôleRisqueCe qu'il mesureNotre noteNature
Token-PermissionsHighMoindre privilège du GITHUB_TOKEN10Acquis
Dangerous-WorkflowCriticalAucun pattern de workflow dangereux10Acquis
Pinned-DependenciesMediumDépendances figées par SHA/digest/hash10Acquis
SASTMediumAnalyse statique sur chaque PR10Acquis
Binary-ArtifactsHighAucun binaire exécutable commité10Acquis
Dependency-Update-ToolHighDependabot ou Renovate présent10Acquis
LicenseLowFichier LICENSE reconnu SPDX10Acquis
PackagingMediumPublication d'artefacts par workflow10Acquis
WebhooksCriticalWebhooks avec secret (ou aucun)10Acquis
Security-PolicyMediumSECURITY.md complet10Acquis (levier)
Signed-ReleasesHighProvenance/signature sur les releases10Acquis (levier)
FuzzingMediumIntégration de fuzzing10Acquis (levier)
CI-TestsLowTests CI verts sur les PR récentes10*Acquis
VulnerabilitiesHighAucun CVE OSV ouvert10*Acquis
Code-ReviewHighChangements approuvés par un tiersvariableLevier
ContributorsLowEntités (Company) distinctesvariableLevier
Branch-ProtectionHighRègles de protection de branchepaliersLevier
CII-Best-PracticesLowBadge bestpractices.dev0Levier
MaintainedHighActivité récente / ancienneté0Plafond

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.

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.

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éclarent permissions: {} 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 checkout en contexte privilégié (pull_request_target suivi 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.yml pousse une image sur GHCR via docker/build-push-action, et la release v1.0.0 prouve 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.yml lance pytest sur chaque pull_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, Trivy et 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 (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 (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 (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 (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 (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 :

fuzz/fuzz_api.py
import sys
import 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: E402
from 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=120

Le score serait le même avec un fichier factice ; la différence est éthique, pas mécanique.

Après les guides précédents, quatre contrôles dépendent encore d'un effort ciblé.

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 (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 (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 (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.

  1. 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).

  2. 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.

  3. 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.

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.

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.

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

6 questions
6 min.
70% requis

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

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

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn