GitHub Actions : le tutoriel pas à pas pour tester et publier une API Node.js
Les six notions de GitHub Actions
Tout workflow GitHub Actions se lit avec six mots, définis dans la page Understanding GitHub Actions de la documentation officielle (consultée le 31 octobre 2026) :
| Notion | Ce que c'est | Dans notre exemple |
|---|---|---|
| Workflow | Un processus automatisé décrit par un fichier YAML dans .github/workflows/ | ci.yml |
| Événement | L'activité du dépôt qui déclenche le workflow | push et pull_request sur main |
| Job | Une suite d'étapes exécutées sur un même runner ; les jobs tournent en parallèle sauf dépendance | test et image |
| Step | Une commande shell ou une action, exécutées dans l'ordre | npm ci, npm test |
| Runner | La machine qui exécute un job, un seul job à la fois | ubuntu-latest, hébergé par GitHub |
| Action | Une extension réutilisable, souvent prise sur le GitHub Marketplace | actions/checkout, docker/login-action |
Chaque exécution part d'une machine virtuelle neuve, et chaque action du Marketplace est du code tiers qui tourne avec vos droits, d'où l'étape 6.
!Schéma du workflow GitHub Actions de ce tutoriel : un événement push ou pull_request déclenche deux jobs de test en parallèle sur Node.js 24 et 26, puis un job qui publie l'image Docker sur ghcr.io
---
Étape 1 : un premier workflow pour l'API Node.js
Le point de départ : récupérer le code, installer Node.js et les dépendances, lancer les tests. L'API du tutoriel déclare "test": "node --test" dans son package.json et exige Node.js 24 ou plus. Créez .github/workflows/ci.yml :
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npm test
npm ci installe exactement ce que fixe package-lock.json, que le dépôt doit contenir, et npm test lance node --test. Le timeout-minutes évite qu'un test bloqué tourne jusqu'à la limite de 6 heures par job des runners hébergés, fixée par la page des limites de GitHub Actions.
Pourquoi checkout v7 et pas v4 ? Selon son README, la v5 est passée au moteur node24, la v6 range les identifiants Git hors de .git/config, et la v7 refuse par défaut le code d'un fork sous pull_request_target ou workflow_run.
Les dernières versions au 31 octobre 2026 sont actions/checkout v7.0.1 et actions/setup-node v7.1.0. L'onglet Actions affiche ensuite chaque exécution ; pour lire les journaux d'un runner Ubuntu, voyez nos commandes Linux de base.
---
Étape 2 : tester sur Node.js 24 et 26 avec une matrice
Une matrice exécute le même job plusieurs fois avec des valeurs différentes. Pour l'API, deux versions comptent : Node.js 24, minimum exigé par le package.json, et Node.js 26, passé en LTS avec la version 26.13.0 du 28 octobre 2026 selon l'index des versions de Node.js. Remplacez le job test :
test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node-version: [24, 26]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
Les deux jobs tournent en parallèle. fail-fast: false laisse finir le second si le premier échoue. La page des limites plafonne une matrice à 256 jobs, et chaque job est facturé séparément.
---
Étape 3 : le cache, plus rapide mais pas sans risque
Le cache de GitHub Actions évite de retélécharger les paquets npm à chaque exécution : ajoutez cache: npm sous with. D'après le README de setup-node, la clé dérive du hachage de package-lock.json et l'action met en cache le dossier global de npm, pas node_modules.
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: npm
Les limites, lues dans la référence du cache de dépendances le 31 octobre 2026 :
- 10 Go par dépôt par défaut. Une entrée non lue depuis plus de 7 jours est supprimée.
- Aucun secret dans le cache. Toute personne ayant un accès en lecture peut ouvrir une pull request et lire un cache.
Le mot-clé cache-mode (read, write, write-only ou none) fixe les droits d'un job sur le cache. Il se place en tête du workflow ou dans un job, par exemple cache-mode: read sous test:, d'après la référence de syntaxe. Selon la documentation du cache consultée le 31 octobre 2026, un déclencheur peu fiable comme pull_request_target n'obtient par défaut que la lecture, et y déclarer write réintroduit le risque d'empoisonnement du cache.
---
Étape 4 : secrets, variables et environnements
Un jeton n'entre jamais dans le YAML : il se range dans Settings, Secrets and variables, Actions, et se lit avec ${{ secrets.NOM }}. Une valeur non sensible, comme une URL, devient une variable lue avec ${{ vars.NOM }}, au niveau du dépôt, de l'organisation ou d'un environnement, selon la page sur les variables.
Pour rattacher le job image de l'étape 5 à un environnement production, avec une variable PRODUCTION_URL créée dans cet environnement, ajoutez au job :
environment:
name: production
url: ${{ vars.PRODUCTION_URL }}
La référence de syntaxe autorise le contexte vars dans url. Mais la page de gestion des environnements réserve les environnements aux dépôts publics sur GitHub Free ; en privé, il faut Pro, Team ou Enterprise. C'est pourquoi le fichier complet plus bas s'en passe.
Notre workflow n'a besoin d'aucun secret ajouté à la main. Les tests de l'API fixent eux-mêmes JWT_SECRET et DB_PATH=':memory:' dans test/api.test.js, et le job image construit l'image sans démarrer le conteneur. Quant au GITHUB_TOKEN, créé pour chaque job, il suffit à publier sur le registre de GitHub.
---
Étape 5 : construire et publier l'image Docker sur GHCR
GHCR, le registre de conteneurs de GitHub (ghcr.io), accepte un push authentifié par GITHUB_TOKEN. L'image reprend le Dockerfile multi-étapes de notre tutoriel Docker pour débutants, basé sur node:24-alpine, placé à la racine du projet. Ajoutez ce job à ci.yml :
image:
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=sha
- uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Ce que fait chaque bloc :
- needs et if. L'image n'est construite que si les deux tests réussissent, et seulement pour un push sur main.
- docker/metadata-action. L'image prend le nom du dépôt, ghcr.io/<compte>/<dépôt>, soit ghcr.io/<compte>/api-taches si le dépôt s'appelle comme le dossier du tutoriel. L'action produit les tags main et sha- suivi du commit abrégé, plus les labels OCI. Son README précise qu'il met le nom d'image en minuscules, ce qu'exige le registre.
- context: . avec checkout. Sans checkout, build-push-action lit le dépôt par Git et ignore les modifications des étapes précédentes, selon son README.
- type=gha. Les couches Docker vont dans le cache de GitHub Actions. Avec plusieurs images, fixez un scope distinct, sinon chaque build écrase le cache du précédent, prévient la documentation de Docker.
Deux pièges documentés par GitHub sur la page du Container registry. Un paquet publié pour la première fois est privé par défaut. Et le GITHUB_TOKEN ne peut pas pousser si une image du même nom a déjà été poussée à la main sans être reliée au dépôt.
Côté coût, la facturation de GitHub Packages indique le 31 octobre 2026 que le stockage du Container registry est gratuit pour l'instant, avec un mois de préavis avant tout changement. L'image se déploie ensuite derrière notre reverse proxy Nginx avec Docker Compose.
---
Étape 6 : GITHUB_TOKEN minimal et actions épinglées
Cette étape a deux volets : limiter ce que le jeton peut faire, et figer le code des actions exécutées.
Le principe du moindre privilège
Le GITHUB_TOKEN est limité au dépôt et valable le temps du job. Ses droits dépendent du réglage du dépôt, puis du bloc permissions. La référence de syntaxe fixe la règle utile : dès que vous citez une permission, toutes celles que vous ne citez pas passent à none.
D'où notre schéma : contents: read au niveau du workflow, packages: write dans le seul job image. C'est ce que récompense le test Token-Permissions d'OpenSSF Scorecard, qui classe ce risque comme élevé. L'option persist-credentials: false de checkout évite en plus de conserver le jeton pour les commandes Git suivantes.
Épingler les actions sur un SHA
Un tag comme @v7 peut être déplacé. Selon l'avis de sécurité GitHub GHSA-mrrh-fwg8-r2c3 (CVE-2025-30066), des attaquants ont redirigé en mars 2025 plusieurs tags de tj-actions/changed-files vers un commit malveillant, qui affichait les secrets dans les journaux entre le 14 et le 15 mars.
La référence de sécurité de GitHub Actions recommande donc d'épingler chaque action sur l'empreinte complète d'un commit, en vérifiant qu'elle vient du dépôt de l'action et non d'un fork. La forme est uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 : l'empreinte fige le code, le commentaire garde la version lisible. Le fichier complet plus bas épingle ainsi les six actions sur les versions publiées au 31 octobre 2026. Nous avons lu ces empreintes sur l'API de GitHub ; vérifiez-les sur la page des versions de chaque action.
Pour ne pas figer des versions vulnérables, laissez Dependabot proposer les mises à jour avec .github/dependabot.yml, selon la documentation de Dependabot :
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Selon la référence de sécurité, les alertes Dependabot ignorent les actions épinglées par SHA ; les mises à jour de version, elles, fonctionnent.
GitHub va plus loin avec les fichiers de verrouillage des workflows : .github/workflows/actions.lock, généré par l'extension gh actions-lock, fige le commit de chaque action, dépendances indirectes comprises, et vérifie l'identité du dépôt source. La référence de sécurité le présente comme la meilleure protection, qui remplace l'épinglage par SHA pour les dépôts qui l'adoptent. Ce tutoriel garde les SHA : la documentation ne dit pas si le dispositif est en préversion ou disponible pour tous, et il exige une extension de GitHub CLI.
---
Étape 7 : concurrence et workflows réutilisables
Deux mots-clés évitent le gaspillage et la copie de YAML. Le premier, concurrency, se place en tête de ci.yml :
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
D'après la page sur la concurrence, un seul workflow s'exécute à la fois par groupe, et une exécution en attente est remplacée par la plus récente. L'expression annule aussi l'exécution en cours, mais seulement pour une pull request. Sur main, l'exécution en cours va à son terme, mais une exécution en attente reste remplacée par la plus récente : seul le dernier push en file est publié.
Le second, workflow_call, transforme un workflow en brique réutilisable. Déplacez le job de test dans .github/workflows/tests-node.yml :
name: Tests Node.js
on:
workflow_call:
inputs:
node-versions:
description: Versions de Node.js, au format JSON
type: string
default: '[24, 26]'
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
strategy:
fail-fast: false
matrix:
node-version: ${{ fromJSON(inputs.node-versions) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test
Dans ci.yml, le job test devient un simple appel, et image garde son needs: test :
jobs:
test:
uses: ./.github/workflows/tests-node.yml
permissions:
contents: read
with:
node-versions: '[24, 26]'
La forme ./.github/workflows/{filename} est celle que la documentation sur la réutilisation donne pour un workflow du même dépôt. La même page, consultée le 31 octobre 2026, mentionne aussi une forme $/.github/workflows/{filename}, qu'elle dit recommandée et indisponible sur GitHub Enterprise Server ; nous ne l'avons pas testée. La référence des workflows réutilisables fixe dix niveaux d'imbrication au plus et 50 workflows réutilisables distincts par fichier ; les variables env de l'appelant ne sont pas transmises, et le workflow appelé ne peut pas élever les permissions reçues.
---
Le fichier ci.yml complet
Voici .github/workflows/ci.yml une fois les étapes 1 à 7 assemblées, avec le job test écrit en entier (sans le workflow réutilisable de l'étape 7 ni l'environnement optionnel de l'étape 4) : déclencheurs, permissions minimales, concurrence, matrice avec cache, publication sur GHCR, et chaque action épinglée sur son SHA. Sa syntaxe a été vérifiée avec le module yaml de Python ; il n'a pas été exécuté sur GitHub. Avec le workflow réutilisable de l'étape 7, le job test ci-dessous est remplacé par l'appel à tests-node.yml.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node-version: [24, 26]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test
image:
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=sha
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.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
---
Combien coûte GitHub Actions en octobre 2026
GitHub Actions reste gratuit pour les dépôts publics sur les runners standards, et sur vos propres machines. Pour les dépôts privés, la page de facturation de GitHub Actions, lue le 31 octobre 2026, exprime désormais le quota en dollars de calcul inclus, et non plus en minutes, déduits selon le tarif de chaque runner :
| Offre | Calcul inclus par mois | Soit, sur Linux 2 cœurs | Stockage d'artefacts | Cache par dépôt |
|---|---|---|---|---|
| GitHub Free | 12 $ | 2 000 minutes | 500 Mo | 10 Go |
| GitHub Pro | 18 $ | 3 000 minutes | 1 Go | 10 Go |
| GitHub Team | 18 $ | 3 000 minutes | 2 Go | 10 Go |
| GitHub Enterprise Cloud | 300 $ | 50 000 minutes | 50 Go | 10 Go |
La conversion est notre calcul à 0,006 $ la minute. Les 2 000 minutes souvent citées, y compris dans notre guide CI/CD, correspondent donc aux 12 $ de l'offre Free sur Linux 2 cœurs ; la page de prix de GitHub affiche bien 2 000, 3 000 et 50 000 minutes pour Free, Team et Enterprise, avec Team à 4 $ et Enterprise à 21 $ par utilisateur et par mois.
Le quota en dollars change la donne dès que vous quittez Linux. La grille des runners facture 0,002 $ la minute sur Linux 1 cœur, 0,005 $ sur Linux arm64 2 cœurs, 0,010 $ sur Windows 2 cœurs et 0,062 $ sur macOS 3 ou 4 cœurs. Les 12 $ de l'offre Free couvrent donc environ 193 minutes de macOS seulement.
!Graphique en barres horizontales GitHub Actions : selon notre calcul, les 12 dollars inclus dans GitHub Free couvrent 6 000 minutes sur Linux 1 cœur, 2 400 sur Linux arm64 2 cœurs, 2 000 sur Linux 2 cœurs, 1 200 sur Windows 2 cœurs et 193 sur macOS 3 ou 4 cœurs
> Notre calcul. Chaque job étant arrondi à la minute supérieure, notre ci.yml coûte au moins 3 minutes par push sur main et 2 par pull request. À 50 de chaque par mois sur un dépôt privé, cela fait au moins 250 minutes, bien dans le quota Free. Une matrice sur trois systèmes d'exploitation changerait tout : une minute de macOS coûte plus de dix minutes de Linux 2 cœurs.
Le stockage se paie à part : 0,25 $ par Go et par mois au-delà de l'allocation d'artefacts, 0,07 $ pour le cache si vous avez relevé sa limite de 10 Go, selon la même page.
---
Runners auto-hébergés : gratuits, pas sans danger
Un runner auto-hébergé est une machine à vous qui exécute les jobs à la place des machines de GitHub. La page de facturation les range, le 31 octobre 2026, parmi les usages gratuits qui ne réduisent pas le calcul inclus. GitHub avait annoncé en décembre 2025 une redevance de 0,002 $ la minute sur les runners auto-hébergés à partir du 1er mars 2026, puis a écrit la reporter « pour prendre le temps de réévaluer notre approche » (notre traduction), sur sa page consacrée aux changements de prix de 2026.
Selon la page sur les runners auto-hébergés, vous payez la machine et mettez à jour vous-même système et outils. Checkout et setup-node, passées au moteur node24, exigent le runner v2.327.1 ou plus selon leurs README. Un job peut y tourner 5 jours d'après la page des limites, mais le GITHUB_TOKEN ne se renouvelle que pendant 24 heures, précise la page GITHUB_TOKEN.
Le vrai risque est ailleurs. Selon la référence de sécurité, un runner auto-hébergé ne garantit pas une machine propre à chaque job et peut être compromis durablement : il ne devrait presque jamais servir à un dépôt public, où n'importe qui peut ouvrir une pull request. Elle mentionne aussi des runners éphémères, dits JIT, qui n'exécutent qu'un job.
Verdict : pour cette API, les runners hébergés suffisent. Notre comparatif de l'hébergement cloud pour développeurs aide à choisir la machine.
---
Ce que nous n'avons pas pu vérifier
Ce tutoriel est une synthèse documentaire. Restent incertains :
- L'exécution des workflows. Leur syntaxe YAML a été vérifiée avec un analyseur Python, mais aucun n'a tourné sur GitHub. Testez-les d'abord sur une branche.
- Le sort de la redevance des runners auto-hébergés. Reportée, mais nous n'avons trouvé aucune annonce qui l'annule.
- Le prix de GitHub Pro. La page de prix lue ne l'affiche pas, ni aucun prix en euros.
- Le statut exact des fichiers de verrouillage. La documentation les décrit sans préciser s'ils sont en préversion ou disponibles pour tous.
- La durée réelle des jobs. Nos chiffres de minutes sont des minimums dus à l'arrondi, pas des mesures.
---
Questions fréquentes
Combien de minutes GitHub Actions gratuites pour un dépôt privé ?
Sur l'offre Free, 12 $ de calcul par mois, soit 2 000 minutes sur le runner Linux 2 cœurs à 0,006 $ la minute, mais 193 seulement sur macOS, selon la page de facturation de GitHub lue le 31 octobre 2026. Les dépôts publics sur les runners standards et les runners auto-hébergés ne consomment rien.
Quelle version d'actions/checkout utiliser ?
La v7 : au 31 octobre 2026, la dernière version publiée est la 7.0.1. Épinglez-la sur son SHA complet, avec la version en commentaire.
Quelle différence entre une action et un workflow ?
Le workflow est votre fichier YAML, déclenché par un événement, qui contient des jobs. Une action est une brique appelée dans un step avec uses, comme actions/setup-node. Un workflow réutilisable s'appelle, lui, au niveau d'un job.
Comment publier une image Docker avec GitHub Actions ?
Connectez-vous à ghcr.io avec docker/login-action et le GITHUB_TOKEN, puis construisez et poussez avec docker/build-push-action, dans un job qui déclare packages: write (étape 5).
---
À propos de l'auteur
Abdelilah Ourti est ingénieur IA et architecte logiciel. Il conçoit et déploie des applications web et des systèmes d'IA, et publie sur NeurauTech des guides et des comparatifs destinés aux développeurs francophones. Ses autres articles sont réunis sur sa page auteur.
Méthode. Synthèse documentaire fondée exclusivement sur des sources publiques consultées le 31 octobre 2026 : documentation de GitHub Actions, de GitHub Packages et de Dependabot, pages de facturation et de prix de GitHub, README et pages de versions des actions officielles et des actions Docker, documentation de Docker, OpenSSF Scorecard, avis de sécurité de GitHub, index des versions de Node.js. Les empreintes SHA ont été lues sur l'API de GitHub. La syntaxe YAML a été vérifiée localement, mais aucun workflow n'a été exécuté. Aucun lien n'est affilié et aucune relation commerciale ne lie l'auteur aux éditeurs mentionnés.
Une erreur ou une version périmée ? Écrivez à contact@neurautech.com : l'article sera corrigé et la date de mise à jour modifiée.
Dernière mise à jour : 31 octobre 2026.
Image : Alberto Rodríguez (Unsplash License).
SBOM Minimum Elements in Practice: What Breaks After the First Release
SBOM Minimum Elements in Practice: What Breaks After the First Release
Software bills of materials (SBOMs) moved from a niche supply-chain practice to a procurement expectation. Generating one is no longer the hard part. Keeping them accurate enough to answer a question during an incident is.
The commonly cited baseline comes from the minimum-elements work that fed into NIST guidance on software supply-chain security, and the fields are unglamorous: supplier name, component name, component version, other unique identifiers, dependency relationship, author of the SBOM data, and a timestamp. Formats such as SPDX and CycloneDX carry those fields. The operational failure modes show up later.
Unique identifiers are where matching succeeds or fails
Two identifier systems dominate practice, and they are not interchangeable. Package URLs describe ecosystems precisely, including namespace, package name and version. Common Platform Enumeration identifiers describe products in a way vulnerability databases understand. A pipeline that emits only ecosystem identifiers will produce SBOMs that read well and correlate badly, because vulnerability matching often relies on product identifiers or on vendor advisories that name a product rather than a package.
The practical fix is to emit both where the tooling supports it, and to record the mapping explicitly. When a tool cannot resolve a product identifier, that gap belongs in the SBOM record rather than being silently dropped, because every downstream match inherits the uncertainty.
Version normalisation and dependency completeness
Build systems rewrite versions. A component pinned as a range, resolved by a lockfile, and patched by a re-resolution step can appear in three different forms across three releases. Normalisation rules need to be written down, and the SBOM needs to state which stage it describes: source, build input, or deployed artifact.
Transitive dependencies are the second gap. An SBOM that lists direct dependencies only will miss the library that actually carries a flaw. Depth limits, vendored copies and statically linked binaries are common blind spots, and generated code from templates or code generators rarely appears at all.
Timestamps, authorship and drift
The timestamp field matters more than it looks. Without it, nobody can tell whether an SBOM describes the artifact currently running or a superseded build. Automating generation at release time, with the artifact hash recorded alongside, removes that ambiguity.
Drift is the quiet problem. SBOMs describe builds; systems run containers, virtual machines and installed packages that change after deployment. Reconciling the two requires either runtime inventory or an honest statement that the SBOM covers the release artifact only. NIST SP 800-218 frames this work as part of secure software development practice rather than a documentation exercise, which is the right framing: the SBOM is only useful if some process consumes it.
Making the data earn its place
Three habits turn an SBOM programme from paperwork into capability. First, validate the SBOM against the build on a schedule, not only when a customer asks. Second, connect it to an asset inventory so that a new advisory can be traced to affected systems rather than affected repositories. Third, record exploitation context for the affected components instead of forwarding raw match lists, because a list of hundreds of matches without triage exhausts the team that has to act on it.
None of this requires exotic tooling. It requires deciding who owns the produced data, what question it must answer, and how quickly. An SBOM that answers one question well beats a complete file nobody queries.
References
- NIST, Secure Software Development Framework (SP 800-218): csrc.nist.gov
- OWASP, Vulnerable Dependency Management Cheat Sheet: cheatsheetseries.owasp.org
- OpenSSF, Scorecard: ossf/scorecard
- CISA, Known Exploited Vulnerabilities Catalog: cisa.gov
1 comment
Mentions
GitHub Actions : le tutoriel pas à pas pour tester et publier une API Node.js Les six notions de GitHub Actions Tout workflow GitHub Actions se lit avec six mots, définis dans la page Understanding GitHub Actions de la documentation officielle (consultée le 31 octobre 2026) : | Notion | Ce que c'est | Dans notre exemple | |---|---|---| | Workflow | Un processus automatisé décrit par un fichier YAML dans .github/workflows/ | ci.yml | | Événement | L'activité du dépôt qui déclenche le workflow | push et pull_request sur main | | Job | Une suite d'étapes exécutées sur un même runner ; les jobs tournent en parallèle sauf dépendance | test et image | | Step | Une commande shell ou une action, exécutées dans l'ordre | npm ci, npm test | | Runner | La machine qui exécute un job, un seul job à la fois | ubuntu-latest, hébergé par GitHub | | Action | Une extension réutilisable, souvent prise sur le GitHub Marketplace | actions/checkout, docker/login-action | Chaque exécution part d'une machine virtuelle neuve, et chaque action du Marketplace est du code tiers qui tourne avec vos droits, d'où l'étape 6. !Schéma du workflow GitHub Actions de ce tutoriel : un événement push ou pull_request déclenche deux jobs de test en parallèle sur Node.js 24 et 26, puis un job qui publie l'image Docker sur ghcr.io --- Étape 1 : un premier workflow pour l'API Node.js Le point de départ : récupérer le code, installer Node.js et les dépendances, lancer les tests. L'API du tutoriel déclare "test": "node --test" dans son package.json et exige Node.js 24 ou plus. Créez .github/workflows/ci.yml : name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read jobs: test: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 24 - run: npm ci - run: npm test npm ci installe exactement ce que fixe package-lock.json, que le dépôt doit contenir, et npm test lance node --test. Le timeout-minutes évite qu'un test bloqué tourne jusqu'à la limite de 6 heures par job des runners hébergés, fixée par la page des limites de GitHub Actions. Pourquoi checkout v7 et pas v4 ? Selon son README, la v5 est passée au moteur node24, la v6 range les identifiants Git hors de .git/config, et la v7 refuse par défaut le code d'un fork sous pull_request_target ou workflow_run. Les dernières versions au 31 octobre 2026 sont actions/checkout v7.0.1 et actions/setup-node v7.1.0. L'onglet Actions affiche ensuite chaque exécution ; pour lire les journaux d'un runner Ubuntu, voyez nos commandes Linux de base. --- Étape 2 : tester sur Node.js 24 et 26 avec une matrice Une matrice exécute le même job plusieurs fois avec des valeurs différentes. Pour l'API, deux versions comptent : Node.js 24, minimum exigé par le package.json, et Node.js 26, passé en LTS avec la version 26.13.0 du 28 octobre 2026 selon l'index des versions de Node.js. Remplacez le job test : test: runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: node-version: [24, 26] steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm test Les deux jobs tournent en parallèle. fail-fast: false laisse finir le second si le premier échoue. La page des limites plafonne une matrice à 256 jobs, et chaque job est facturé séparément. --- Étape 3 : le cache, plus rapide mais pas sans risque Le cache de GitHub Actions évite de retélécharger les paquets npm à chaque exécution : ajoutez cache: npm sous with. D'après le README de setup-node, la clé dérive du hachage de package-lock.json et l'action met en cache le dossier global de npm, pas node_modules. - uses: actions/setup-node@v7 with: node-version: ${{ matrix.node-version }} cache: npm Les limites, lues dans la référence du cache de dépendances le 31 octobre 2026 : - 10 Go par dépôt par défaut. Une entrée non lue depuis plus de 7 jours est supprimée. - Aucun secret dans le cache. Toute personne ayant un accès en lecture peut ouvrir une pull request et lire un cache. Le mot-clé cache-mode (read, write, write-only ou none) fixe les droits d'un job sur le cache. Il se place en tête du workflow ou dans un job, par exemple cache-mode: read sous test:, d'après la référence de syntaxe. Selon la documentation du cache consultée le 31 octobre 2026, un déclencheur peu fiable comme pull_request_target n'obtient par défaut que la lecture, et y déclarer write réintroduit le risque d'empoisonnement du cache. --- Étape 4 : secrets, variables et environnements Un jeton n'entre jamais dans le YAML : il se range dans Settings, Secrets and variables, Actions, et se lit avec ${{ secrets.NOM }}. Une valeur non sensible, comme une URL, devient une variable lue avec ${{ vars.NOM }}, au niveau du dépôt, de l'organisation ou d'un environnement, selon la page sur les variables. Pour rattacher le job image de l'étape 5 à un environnement production, avec une variable PRODUCTION_URL créée dans cet environnement, ajoutez au job : environment: name: production url: ${{ vars.PRODUCTION_URL }} La référence de syntaxe autorise le contexte vars dans url. Mais la page de gestion des environnements réserve les environnements aux dépôts publics sur GitHub Free ; en privé, il faut Pro, Team ou Enterprise. C'est pourquoi le fichier complet plus bas s'en passe. Notre workflow n'a besoin d'aucun secret ajouté à la main. Les tests de l'API fixent eux-mêmes JWT_SECRET et DB_PATH=':memory:' dans test/api.test.js, et le job image construit l'image sans démarrer le conteneur. Quant au GITHUB_TOKEN, créé pour chaque job, il suffit à publier sur le registre de GitHub. --- Étape 5 : construire et publier l'image Docker sur GHCR GHCR, le registre de conteneurs de GitHub (ghcr.io), accepte un push authentifié par GITHUB_TOKEN. L'image reprend le Dockerfile multi-étapes de notre tutoriel Docker pour débutants, basé sur node:24-alpine, placé à la racine du projet. Ajoutez ce job à ci.yml : image: needs: test if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read packages: write steps: - uses: actions/checkout@v7 with: persist-credentials: false - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=sha - uses: docker/build-push-action@v7 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max Ce que fait chaque bloc : - needs et if. L'image n'est construite que si les deux tests réussissent, et seulement pour un push sur main. - docker/metadata-action. L'image prend le nom du dépôt, ghcr.io/<compte>/<dépôt>, soit ghcr.io/<compte>/api-taches si le dépôt s'appelle comme le dossier du tutoriel. L'action produit les tags main et sha- suivi du commit abrégé, plus les labels OCI. Son README précise qu'il met le nom d'image en minuscules, ce qu'exige le registre. - context: . avec checkout. Sans checkout, build-push-action lit le dépôt par Git et ignore les modifications des étapes précédentes, selon son README. - type=gha. Les couches Docker vont dans le cache de GitHub Actions. Avec plusieurs images, fixez un scope distinct, sinon chaque build écrase le cache du précédent, prévient la documentation de Docker. Deux pièges documentés par GitHub sur la page du Container registry. Un paquet publié pour la première fois est privé par défaut. Et le GITHUB_TOKEN ne peut pas pousser si une image du même nom a déjà été poussée à la main sans être reliée au dépôt. Côté coût, la facturation de GitHub Packages indique le 31 octobre 2026 que le stockage du Container registry est gratuit pour l'instant, avec un mois de préavis avant tout changement. L'image se déploie ensuite derrière notre reverse proxy Nginx avec Docker Compose. --- Étape 6 : GITHUB_TOKEN minimal et actions épinglées Cette étape a deux volets : limiter ce que le jeton peut faire, et figer le code des actions exécutées. Le principe du moindre privilège Le GITHUB_TOKEN est limité au dépôt et valable le temps du job. Ses droits dépendent du réglage du dépôt, puis du bloc permissions. La référence de syntaxe fixe la règle utile : dès que vous citez une permission, toutes celles que vous ne citez pas passent à none. D'où notre schéma : contents: read au niveau du workflow, packages: write dans le seul job image. C'est ce que récompense le test Token-Permissions d'OpenSSF Scorecard, qui classe ce risque comme élevé. L'option persist-credentials: false de checkout évite en plus de conserver le jeton pour les commandes Git suivantes. Épingler les actions sur un SHA Un tag comme @v7 peut être déplacé. Selon l'avis de sécurité GitHub GHSA-mrrh-fwg8-r2c3 (CVE-2025-30066), des attaquants ont redirigé en mars 2025 plusieurs tags de tj-actions/changed-files vers un commit malveillant, qui affichait les secrets dans les journaux entre le 14 et le 15 mars. La référence de sécurité de GitHub Actions recommande donc d'épingler chaque action sur l'empreinte complète d'un commit, en vérifiant qu'elle vient du dépôt de l'action et non d'un fork. La forme est uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 : l'empreinte fige le code, le commentaire garde la version lisible. Le fichier complet plus bas épingle ainsi les six actions sur les versions publiées au 31 octobre 2026. Nous avons lu ces empreintes sur l'API de GitHub ; vérifiez-les sur la page des versions de chaque action. Pour ne pas figer des versions vulnérables, laissez Dependabot proposer les mises à jour avec .github/dependabot.yml, selon la documentation de Dependabot : version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" Selon la référence de sécurité, les alertes Dependabot ignorent les actions épinglées par SHA ; les mises à jour de version, elles, fonctionnent. GitHub va plus loin avec les fichiers de verrouillage des workflows : .github/workflows/actions.lock, généré par l'extension gh actions-lock, fige le commit de chaque action, dépendances indirectes comprises, et vérifie l'identité du dépôt source. La référence de sécurité le présente comme la meilleure protection, qui remplace l'épinglage par SHA pour les dépôts qui l'adoptent. Ce tutoriel garde les SHA : la documentation ne dit pas si le dispositif est en préversion ou disponible pour tous, et il exige une extension de GitHub CLI. --- Étape 7 : concurrence et workflows réutilisables Deux mots-clés évitent le gaspillage et la copie de YAML. Le premier, concurrency, se place en tête de ci.yml : concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} D'après la page sur la concurrence, un seul workflow s'exécute à la fois par groupe, et une exécution en attente est remplacée par la plus récente. L'expression annule aussi l'exécution en cours, mais seulement pour une pull request. Sur main, l'exécution en cours va à son terme, mais une exécution en attente reste remplacée par la plus récente : seul le dernier push en file est publié. Le second, workflow_call, transforme un workflow en brique réutilisable. Déplacez le job de test dans .github/workflows/tests-node.yml : name: Tests Node.js on: workflow_call: inputs: node-versions: description: Versions de Node.js, au format JSON type: string default: '[24, 26]' jobs: test: runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read strategy: fail-fast: false matrix: node-version: ${{ fromJSON(inputs.node-versions) }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node-version }} cache: npm - run: npm ci - run: npm test Dans ci.yml, le job test devient un simple appel, et image garde son needs: test : jobs: test: uses: ./.github/workflows/tests-node.yml permissions: contents: read with: node-versions: '[24, 26]' La forme ./.github/workflows/{filename} est celle que la documentation sur la réutilisation donne pour un workflow du même dépôt. La même page, consultée le 31 octobre 2026, mentionne aussi une forme $/.github/workflows/{filename}, qu'elle dit recommandée et indisponible sur GitHub Enterprise Server ; nous ne l'avons pas testée. La référence des workflows réutilisables fixe dix niveaux d'imbrication au plus et 50 workflows réutilisables distincts par fichier ; les variables env de l'appelant ne sont pas transmises, et le workflow appelé ne peut pas élever les permissions reçues. --- Le fichier ci.yml complet Voici .github/workflows/ci.yml une fois les étapes 1 à 7 assemblées, avec le job test écrit en entier (sans le workflow réutilisable de l'étape 7 ni l'environnement optionnel de l'étape 4) : déclencheurs, permissions minimales, concurrence, matrice avec cache, publication sur GHCR, et chaque action épinglée sur son SHA. Sa syntaxe a été vérifiée avec le module yaml de Python ; il n'a pas été exécuté sur GitHub. Avec le workflow réutilisable de l'étape 7, le job test ci-dessous est remplacé par l'appel à tests-node.yml. name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: test: runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: node-version: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node-version }} cache: npm - run: npm ci - run: npm test image: needs: test if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read packages: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=sha - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.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 --- Combien coûte GitHub Actions en octobre 2026 GitHub Actions reste gratuit pour les dépôts publics sur les runners standards, et sur vos propres machines. Pour les dépôts privés, la page de facturation de GitHub Actions, lue le 31 octobre 2026, exprime désormais le quota en dollars de calcul inclus, et non plus en minutes, déduits selon le tarif de chaque runner : | Offre | Calcul inclus par mois | Soit, sur Linux 2 cœurs | Stockage d'artefacts | Cache par dépôt | |---|---|---|---|---| | GitHub Free | 12 $ | 2 000 minutes | 500 Mo | 10 Go | | GitHub Pro | 18 $ | 3 000 minutes | 1 Go | 10 Go | | GitHub Team | 18 $ | 3 000 minutes | 2 Go | 10 Go | | GitHub Enterprise Cloud | 300 $ | 50 000 minutes | 50 Go | 10 Go | La conversion est notre calcul à 0,006 $ la minute. Les 2 000 minutes souvent citées, y compris dans notre guide CI/CD, correspondent donc aux 12 $ de l'offre Free sur Linux 2 cœurs ; la page de prix de GitHub affiche bien 2 000, 3 000 et 50 000 minutes pour Free, Team et Enterprise, avec Team à 4 $ et Enterprise à 21 $ par utilisateur et par mois. Le quota en dollars change la donne dès que vous quittez Linux. La grille des runners facture 0,002 $ la minute sur Linux 1 cœur, 0,005 $ sur Linux arm64 2 cœurs, 0,010 $ sur Windows 2 cœurs et 0,062 $ sur macOS 3 ou 4 cœurs. Les 12 $ de l'offre Free couvrent donc environ 193 minutes de macOS seulement. !Graphique en barres horizontales GitHub Actions : selon notre calcul, les 12 dollars inclus dans GitHub Free couvrent 6 000 minutes sur Linux 1 cœur, 2 400 sur Linux arm64 2 cœurs, 2 000 sur Linux 2 cœurs, 1 200 sur Windows 2 cœurs et 193 sur macOS 3 ou 4 cœurs > Notre calcul. Chaque job étant arrondi à la minute supérieure, notre ci.yml coûte au moins 3 minutes par push sur main et 2 par pull request. À 50 de chaque par mois sur un dépôt privé, cela fait au moins 250 minutes, bien dans le quota Free. Une matrice sur trois systèmes d'exploitation changerait tout : une minute de macOS coûte plus de dix minutes de Linux 2 cœurs. Le stockage se paie à part : 0,25 $ par Go et par mois au-delà de l'allocation d'artefacts, 0,07 $ pour le cache si vous avez relevé sa limite de 10 Go, selon la même page. --- Runners auto-hébergés : gratuits, pas sans danger Un runner auto-hébergé est une machine à vous qui exécute les jobs à la place des machines de GitHub. La page de facturation les range, le 31 octobre 2026, parmi les usages gratuits qui ne réduisent pas le calcul inclus. GitHub avait annoncé en décembre 2025 une redevance de 0,002 $ la minute sur les runners auto-hébergés à partir du 1er mars 2026, puis a écrit la reporter « pour prendre le temps de réévaluer notre approche » (notre traduction), sur sa page consacrée aux changements de prix de 2026. Selon la page sur les runners auto-hébergés, vous payez la machine et mettez à jour vous-même système et outils. Checkout et setup-node, passées au moteur node24, exigent le runner v2.327.1 ou plus selon leurs README. Un job peut y tourner 5 jours d'après la page des limites, mais le GITHUB_TOKEN ne se renouvelle que pendant 24 heures, précise la page GITHUB_TOKEN. Le vrai risque est ailleurs. Selon la référence de sécurité, un runner auto-hébergé ne garantit pas une machine propre à chaque job et peut être compromis durablement : il ne devrait presque jamais servir à un dépôt public, où n'importe qui peut ouvrir une pull request. Elle mentionne aussi des runners éphémères, dits JIT, qui n'exécutent qu'un job. Verdict : pour cette API, les runners hébergés suffisent. Notre comparatif de l'hébergement cloud pour développeurs aide à choisir la machine. --- Ce que nous n'avons pas pu vérifier Ce tutoriel est une synthèse documentaire. Restent incertains : - L'exécution des workflows. Leur syntaxe YAML a été vérifiée avec un analyseur Python, mais aucun n'a tourné sur GitHub. Testez-les d'abord sur une branche. - Le sort de la redevance des runners auto-hébergés. Reportée, mais nous n'avons trouvé aucune annonce qui l'annule. - Le prix de GitHub Pro. La page de prix lue ne l'affiche pas, ni aucun prix en euros. - Le statut exact des fichiers de verrouillage. La documentation les décrit sans préciser s'ils sont en préversion ou disponibles pour tous. - La durée réelle des jobs. Nos chiffres de minutes sont des minimums dus à l'arrondi, pas des mesures. --- Questions fréquentes Combien de minutes GitHub Actions gratuites pour un dépôt privé ? Sur l'offre Free, 12 $ de calcul par mois, soit 2 000 minutes sur le runner Linux 2 cœurs à 0,006 $ la minute, mais 193 seulement sur macOS, selon la page de facturation de GitHub lue le 31 octobre 2026. Les dépôts publics sur les runners standards et les runners auto-hébergés ne consomment rien. Quelle version d'actions/checkout utiliser ? La v7 : au 31 octobre 2026, la dernière version publiée est la 7.0.1. Épinglez-la sur son SHA complet, avec la version en commentaire. Quelle différence entre une action et un workflow ? Le workflow est votre fichier YAML, déclenché par un événement, qui contient des jobs. Une action est une brique appelée dans un step avec uses, comme actions/setup-node. Un workflow réutilisable s'appelle, lui, au niveau d'un job. Comment publier une image Docker avec GitHub Actions ? Connectez-vous à ghcr.io avec docker/login-action et le GITHUB_TOKEN, puis construisez et poussez avec docker/build-push-action, dans un job qui déclare packages: write (étape 5). --- À propos de l'auteur Abdelilah Ourti est ingénieur IA et architecte logiciel. Il conçoit et déploie des applications web et des systèmes d'IA, et publie sur NeurauTech des guides et des comparatifs destinés aux développeurs francophones. Ses autres articles sont réunis sur sa page auteur. Méthode. Synthèse documentaire fondée exclusivement sur des sources publiques consultées le 31 octobre 2026 : documentation de GitHub Actions, de GitHub Packages et de Dependabot, pages de facturation et de prix de GitHub, README et pages de versions des actions officielles et des actions Docker, documentation de Docker, OpenSSF Scorecard, avis de sécurité de GitHub, index des versions de Node.js. Les empreintes SHA ont été lues sur l'API de GitHub. La syntaxe YAML a été vérifiée localement, mais aucun workflow n'a été exécuté. Aucun lien n'est affilié et aucune relation commerciale ne lie l'auteur aux éditeurs mentionnés. Une erreur ou une version périmée ? Écrivez à contact@neurautech.com : l'article sera corrigé et la date de mise à jour modifiée. Dernière mise à jour : 31 octobre 2026. Image : Alberto Rodríguez (Unsplash License).
このPRがマージされれば解消するんだが、一旦dismissするか scorecard#5247
Zennの原稿リポジトリのセキュリティ対策を、OWASPなどのガイドラインと照らし合わせた Zennの記事の原稿は、GitHubのリポジトリで管理している。 ZennのGitHub連携を使っているので、mainブランチにpushした原稿がそのまま公開される。 個人の小さなリポジトリだが、pushひとつで外に出るという点では本番環境へのデプロイと変わらない。 依存パッケージの更新はDependabotに、原稿の編集の一部はClaude Code(AIエージェント)に任せている。 どちらも、自分以外が原稿のリポジトリを変更する経路である。 悪意のあるパッケージが紛れ込んだり、エージェントが意図しない操作をしたりすれば、その結果が自分の名前で公開されかねない。 そのため、この2つの経路には対策を入れている。 ただ、自分の感覚で決めた対策には、抜けがあるかもしれない。 そこで、入れている対策を、公開されているガイドラインの項目と照らし合わせた。 この記事では、照らし合わせたガイドラインと、それぞれに対応する対策を書く。 守りたいものとリスク 守りたいものは2つある。 自分の名前で公開される内容:意図しない原稿や変更が公開されたり、公開済みの記事が消えたりしないこと 手元の開発環境:npm ciやGitフックで実行される依存パッケージのコードに、悪意のあるコードが混ざらないこと これに対するリスクは次の2つだと考えている。 依存パッケージに悪意のあるバージョンが公開され、それを取り込んでしまう AIエージェントが、意図しない公開や削除をしてしまう 照らし合わせたガイドライン OWASPとは OWASP(Open Worldwide Application Security Project)は、ソフトウェアのセキュリティ向上に取り組む非営利団体である。 2001年に始まり、現在は米国の非営利法人OWASP Foundationが運営している。 世界中のボランティアが、文書やツールをオープンソースのプロジェクトとして作っている。 成果物はすべて無料で公開されており、誰でも読める。 次に紹介するOWASP Top 10とOWASP Top 10 for LLM Applicationsも、OWASPのプロジェクトである。 owasp.org OWASP Top 10:2025 Webアプリケーションの代表的なリスクをまとめた文書である。 2025年版では「A03:2025 Software Supply Chain Failures」が3位に入っている。 依存パッケージやビルドの経路を通じた攻撃を、独立したリスクとして扱っている。 top10.owasp.org OWASP Top 10 for LLM Applications 2025 LLMを組み込んだアプリケーションのリスクをまとめた文書である。 このうち「LLM06:2025 Excessive Agency」が、AIエージェントに作業を任せるときの指針になる。 原因として、機能、権限、自律性の3つが過剰であることを挙げている。 対策として、使えるツールを最小限にすること、影響の大きい操作には人の承認を挟むこと、操作の可否をLLMに判断させず外側の仕組みで決めることが書かれている。 genai.owasp.org OpenSSF Scorecard OpenSSF(Open Source Security Foundation)は、オープンソースソフトウェアのセキュリティ向上に取り組む団体で、2020年にLinux Foundationのもとで発足した。 Scorecardはそのプロジェクトの1つで、OSSリポジトリのセキュリティ上の慣行を自動で採点するツールである。 チェック項目の一覧が、そのまま点検表として使える。 この記事では、Dependency-Update-Toolの項目を参照する。 linuxfoundation.org github.com/ossf/scorecard/blob/main/docs/checks.md 対策1:依存パッケージはDependabotのPRで週に1回更新する 対応する項目:OWASP Top 10:2025のA03、ScorecardのDependency-Update-Tool 依存パッケージの更新はDependabotに任せている。 .github/dependabot.yml version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" groups: commitlint: patterns: - "@commitlint/*" 更新は週に1回、PRとして届く。 commitlint関連のパッケージは同時に更新しないと噛み合わないことがあるので、groupsで1つのPRにまとめている。 PRは自分で中身を見てからマージする。 package-lock.jsonはコミットしており、手元ではnpm installではなくnpm ciを使う。 lockfileに書かれたバージョンとハッシュのとおりにインストールされるので、Dependabotのマージを経ずに依存が変わることはない。 公開直後のバージョンをすぐに取り込まないことも、サプライチェーン攻撃への備えになる。 乗っ取られたアカウントから悪意のあるバージョンが公開されても、発見されて取り下げられるまでの間に取り込まなければ済むからである。 GitHubのドキュメントによると、Dependabotはバージョン更新に既定で3日のクールダウンを設けている。 日数はcooldownオプションで変えられる。 docs.github.com 対策2:AIエージェントの権限を絞り、pushの前に確認を挟む 対応する項目:OWASP Top 10 for LLM Applications 2025のLLM06 Claude Codeでは、プロジェクトの.claude/settings.jsonでツールの権限を設定できる。 設定は次のとおりである。 .claude/settings.json(抜粋) { "permissions": { "allow": [ "Bash(npx prettier:*)", "Bash(npx zenn preview:*)", "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", "Bash(git show:*)", "Bash(node .claude/skills/zenn-check/check.mjs:*)" ], "ask": ["Bash(git push:*)"] } } allowに入れているのは、読み取りと検証のコマンドだけである。 これらは確認なしで実行される。 git pushはaskに入れ、エージェントがgit pushを実行する前に、自分の承認を求められるようにしている。 mainへのpushがそのまま公開になるので、LLM06の「影響の大きい操作には人の承認を挟む」に当たる。 リポジトリのルールを書いたAGENTS.mdにも、mainへのpushと、published: falseからtrueへの変更は、依頼されたときだけ行うよう書いている。 ただし、これはエージェントへの指示であって、守られる保証はない。 そのため、pushを止めるかどうかは、askという外側の仕組みで決めている。 これは、LLM06の「操作の可否をLLMに判断させず外側の仕組みで決める」にも当たる。 もっとも、askで止められるのは、git push origin mainのような普通の書き方だけである。 Claude Codeのドキュメントには、git -C . push origin mainのように書き方を変えると、git pushのルールに一致しないと書かれている。 同じドキュメントは、Bashのルールはプログラムの周りのセキュリティ境界にはならないとも書いている。 そのため、この設定はエージェントの誤操作を減らすためのものと位置づけている。 code.claude.com まとめ 照らし合わせたガイドラインの項目と、対応する対策を表にまとめる。 ガイドラインの項目 対策 OWASP Top 10:2025 A03、Scorecard Dependency-Update-Tool DependabotのPRで週1回更新、npm ci LLM06:2025 Excessive Agency 権限を読み取りと検証に限定、git pushの前に確認 ガイドラインの項目と照らし合わせておくと、それぞれの対策が何に備えたものかを説明できる。
GitHub Actions Security Audit: A 30 Minute Pipeline Checklist A thirty-minute audit, our own framework and not an industry standard, covers token permissions, risky triggers, secrets, caches, action pinning, npm publishing, install defaults and alerting on your own publishes. Detection and response matter too: rotate exposed credentials, and treat tools such as Scorecard and SLSA as prompts, not guarantees. Episode 7 of 7 in the series 'CI/CD Supply Chain Security: GitHub Actions and npm After the 2026 Attacks'. CHAPTERS 0:00 Who notices first? 0:44 Log what leaves the runner 1:31 Assume exposed, then rotate 2:30 Tools help, but have limits 3:25 Minutes zero to fifteen 4:19 Minutes fifteen to thirty 5:13 Seven episodes, one checklist QUICK ANSWERS Q: How do I respond if a compromised package was installed? A: TanStack's postmortem recommends rotating cloud, Kubernetes, Vault, GitHub, npm and SSH credentials reachable from the install host, and CISA's tj-actions alert says to rotate identified secrets immediately. Q: What does the GitHub Actions network firewall do? A: GitHub's July 2026 post says the technical preview logs all outbound network traffic from workflow runs to detect unusual behaviour, with egress restrictions described as future work (status as of that post; re-check). Q: What can OpenSSF Scorecard and SLSA tell me? A: Scorecard has checks such as Dangerous-Workflow and Token-Permissions for workflow risks. SLSA describes incrementally adoptable supply chain guidelines and states that it does not address organizations that intentionally produce malicious software. KEY FACTS AND SOURCES - TanStack postmortem: no internal alerting; the compromise was learned of from a third party. - GitHub (July 2026): the Actions network firewall, in technical preview at that time, logs all outbound traffic from workflow runs. - SLSA documentation: SLSA does not address organizations that intentionally produce malicious software. Source: Postmortem: TanStack npm supply-chain compromise (TanStack, 2026-05-11): tanstack.com Source: Disrupting supply chain attacks on npm and GitHub Actions (GitHub Blog, 2026-07-28): github.blog Source: Secure use reference (GitHub Docs): docs.github.com Source: Supply Chain Compromise of Third-Party tj-actions/changed-files (CISA): cisa.gov Source: OpenSSF Scorecard checks: github.com/ossf/scorecard/blob/main/docs/checks.md Source: About SLSA (SLSA specification v1.2): slsa.dev Source: Trusted publishing for npm packages (npm Docs): docs.npmjs.com SERIES Previous episode: youtu.be Full series playlist: youtube.com Figures and claims in this series come from public job-posting analyses, career guides and interview-prep sites. They are approximate and change quickly, so check current postings before making decisions. Disclosure: the narrator's voice and the on-screen presenter are AI-generated (synthetic media). #GitHubActions #DevSecOps #CICD
Pin GitHub Actions to a SHA: Lockfiles and Immutable Releases Tags and branches can move, so GitHub's documentation recommends pinning actions to a full-length commit SHA, and says workflow dependency lockfiles, in public preview as of October 2026, go further by covering transitive dependencies. Publishers can use immutable releases so tags and assets cannot change. Episode 3 of 7 in the series 'CI/CD Supply Chain Security: GitHub Actions and npm After the 2026 Attacks'. CHAPTERS 0:00 Tags can move 0:45 A hash fixes the code 1:29 Pinning is not the whole answer 2:19 Lock every action, even nested 3:12 Enforce it, but pilot first 3:52 Publishers can lock releases too 4:43 Recap and next QUICK ANSWERS Q: Should I pin GitHub Actions to a commit SHA? A: GitHub's secure use documentation says to pin third-party actions to a full-length commit SHA, verifying the SHA comes from the action's own repository. It also says tags can be moved or deleted if the repository is compromised, and its lockfile documentation says SHA pinning in workflow files is no longer necessary for repositories that adopt a lockfile. Q: What is a GitHub Actions workflow dependency lockfile? A: A file generated by a GitHub CLI extension that pins a commit SHA to every action, including transitive dependencies, and every reusable workflow. GitHub's documentation says it is in public preview and subject to change (as of October 2026). Q: What are immutable releases? A: GitHub's documentation describes releases whose assets and Git tag cannot be changed after publication, with an automatically generated release attestation. KEY FACTS AND SOURCES - GitHub Docs: a tag can be moved or deleted if a bad actor gains access to the repository storing the action. - GitHub (March 2026 roadmap): commit SHAs help but are hard to manage at scale and transitive dependencies remain opaque. - GitHub Docs: workflow dependency lockfiles are in public preview and subject to change (as of October 2026). Source: Secure use reference (GitHub Docs): docs.github.com Source: Workflow dependency lockfiles (GitHub Docs, public preview): docs.github.com Source: Enforcing workflow lockfiles (GitHub Docs): docs.github.com Source: Immutable releases (GitHub Docs): docs.github.com Source: What's coming to our GitHub Actions 2026 security roadmap (GitHub Blog, 2026-03-26): github.blog Source: OpenSSF Scorecard checks: github.com/ossf/scorecard/blob/main/docs/checks.md SERIES Previous episode: youtu.be Full series playlist: youtube.com Figures and claims in this series come from public job-posting analyses, career guides and interview-prep sites. They are approximate and change quickly, so check current postings before making decisions. Disclosure: the narrator's voice and the on-screen presenter are AI-generated (synthetic media). #GitHubActions #DevSecOps #SupplyChainSecurity
SBOM Minimum Elements in Practice: What Breaks After the First Release SBOM Minimum Elements in Practice: What Breaks After the First Release Software bills of materials (SBOMs) moved from a niche supply-chain practice to a procurement expectation. Generating one is no longer the hard part. Keeping them accurate enough to answer a question during an incident is. The commonly cited baseline comes from the minimum-elements work that fed into NIST guidance on software supply-chain security, and the fields are unglamorous: supplier name, component name, component version, other unique identifiers, dependency relationship, author of the SBOM data, and a timestamp. Formats such as SPDX and CycloneDX carry those fields. The operational failure modes show up later. Unique identifiers are where matching succeeds or fails Two identifier systems dominate practice, and they are not interchangeable. Package URLs describe ecosystems precisely, including namespace, package name and version. Common Platform Enumeration identifiers describe products in a way vulnerability databases understand. A pipeline that emits only ecosystem identifiers will produce SBOMs that read well and correlate badly, because vulnerability matching often relies on product identifiers or on vendor advisories that name a product rather than a package. The practical fix is to emit both where the tooling supports it, and to record the mapping explicitly. When a tool cannot resolve a product identifier, that gap belongs in the SBOM record rather than being silently dropped, because every downstream match inherits the uncertainty. Version normalisation and dependency completeness Build systems rewrite versions. A component pinned as a range, resolved by a lockfile, and patched by a re-resolution step can appear in three different forms across three releases. Normalisation rules need to be written down, and the SBOM needs to state which stage it describes: source, build input, or deployed artifact. Transitive dependencies are the second gap. An SBOM that lists direct dependencies only will miss the library that actually carries a flaw. Depth limits, vendored copies and statically linked binaries are common blind spots, and generated code from templates or code generators rarely appears at all. Timestamps, authorship and drift The timestamp field matters more than it looks. Without it, nobody can tell whether an SBOM describes the artifact currently running or a superseded build. Automating generation at release time, with the artifact hash recorded alongside, removes that ambiguity. Drift is the quiet problem. SBOMs describe builds; systems run containers, virtual machines and installed packages that change after deployment. Reconciling the two requires either runtime inventory or an honest statement that the SBOM covers the release artifact only. NIST SP 800-218 frames this work as part of secure software development practice rather than a documentation exercise, which is the right framing: the SBOM is only useful if some process consumes it. Making the data earn its place Three habits turn an SBOM programme from paperwork into capability. First, validate the SBOM against the build on a schedule, not only when a customer asks. Second, connect it to an asset inventory so that a new advisory can be traced to affected systems rather than affected repositories. Third, record exploitation context for the affected components instead of forwarding raw match lists, because a list of hundreds of matches without triage exhausts the team that has to act on it. None of this requires exotic tooling. It requires deciding who owns the produced data, what question it must answer, and how quickly. An SBOM that answers one question well beats a complete file nobody queries. References - NIST, Secure Software Development Framework (SP 800-218): csrc.nist.gov - OWASP, Vulnerable Dependency Management Cheat Sheet: cheatsheetseries.owasp.org - OpenSSF, Scorecard: ossf/scorecard - CISA, Known Exploited Vulnerabilities Catalog: cisa.gov
OpenSSF Scorecard v5.6.0 and scorecard-action v2.5.0 are out! 🎉 This is the first Scorecard release since our hosted services (the weekly scans and the REST API) moved to AboutCode Foundation infra! If you use the GitHub Action, upgrade to v2.5.0! Scorecard v5.6.0: github.com/ossf/scorecard/releases/tag/v5.6.0
Dependency Verification: What Pinning Buys and What It Does Not Dependency Verification: What Pinning Buys and What It Does Not Pinning dependencies to exact versions is the most widely adopted supply chain control, and it is frequently described as though it prevents a malicious package from reaching production. It does not. Exact versions make builds reproducible, and reproducibility is a prerequisite for verification rather than a substitute for it. What pinning changes A lock file with exact versions and content hashes makes the resolved dependency graph deterministic. The same lock file produces the same set of artefacts on a developer machine and in the pipeline, which removes version drift as a source of inconsistency. That determinism has a security consequence. A package republished under the same version number with different content breaks the hash check rather than being installed silently. Without a lock file and hash verification, that substitution is invisible. The limit of the control is visible in the same example. A malicious package published under a new version number is not affected by pinning, and a transitive dependency that is not pinned is resolved at install time. Teams that pin direct dependencies and ignore transitive ones have closed part of the gap. Verification beyond the lock file Signature verification answers a different question than hashing. A hash proves the artefact is the one that was reviewed. A signature proves which key produced the artefact, and it allows a consumer to apply a policy about which producers are acceptable. cosign verify --certificate-identity-regexp '^https://github.com/example/.*$' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ registry.example.com/app@sha256:0000000000000000000000000000000000000000000000000000000000000000 Provenance goes further. A build attestation records what source revision produced an artefact, by which build process, and from which inputs. SLSA describes levels of this capability, from a documented build process through to a hardened one that produces non-falsifiable provenance. OSSF Scorecard produces a related view by scoring repository practices such as branch protection and review requirements, which is useful as a triage signal for a large dependency tree. An SBOM complements all of the above by listing what is inside an artefact. Its value depends on what it is used for. An SBOM that is generated and stored without being compared to a vulnerability feed or a policy list is an audit artefact rather than a control. Where the process breaks Three failure points account for most of the gap between the described process and the one that runs. The verification step is optional, so a pipeline continues when it fails. The allowed producers are defined in a list nobody updates, so a legitimate new signer is blocked and a developer disables the check. And the dependency update automation merges version bumps without distinguishing a patch bump from a change in the publishing identity. The last point deserves attention because update automation is where pinning and verification interact. An automated update that changes a version is fine when the producer identity is verified. An automated update that accepts any publisher is a mechanism for installing whatever is published next. References - SLSA specification, Levels: slsa.dev - OSSF Scorecard: ossf/scorecard - CycloneDX: cyclonedx.org - SPDX: spdx.dev