Un SBOM CycloneDX est l’inventaire machine des composants d’un logiciel, avec leurs versions et leurs identifiants. Pour une application conteneurisée, il en faut deux : un SBOM du code, produit à partir du lockfile par cdxgen, et un SBOM de chaque image, produit par syft, qui voit aussi les paquets système. Cet article explique pourquoi les deux sont nécessaires, comment les générer, comment les scanner avec grype, et ce qu’il faut en faire ensuite.

Qu’est-ce qu’un SBOM ?

Un SBOM (Software Bill of Materials) est la liste structurée de tout ce qu’embarque un logiciel : bibliothèques, paquets du système d’exploitation, binaires, avec pour chacun une version et un identifiant standard. Cet identifiant est le plus souvent un purl (package URL), par exemple pkg:npm/esbuild@0.25.1 ou pkg:apk/alpine/openssl@..., ce qui permet à un outil de le comparer à une base de vulnérabilités sans ambiguïté.

Un SBOM n’est pas un rapport de vulnérabilités. Il décrit ce qui est présent ; l’analyse vient après, et peut être refaite à volonté sur le même inventaire.

Pourquoi choisir le format CycloneDX ?

CycloneDX est un format de SBOM porté par l’OWASP et orienté sécurité, qui sait représenter nativement le VEX (l’exploitabilité d’une vulnérabilité). Il se sérialise en JSON ou en XML, et sa version de spécification est indiquée dans chaque document (specVersion).

L’alternative principale est SPDX, plus orientée licences. Les deux sont reconnus ; le choix dépend surtout des outils en aval. Dependency-Track, grype et syft lisent CycloneDX nativement.

Un point pratique compte : la version de spécification. Un outil récent peut émettre une version que l’outil en aval ne lit pas encore. Fixer explicitement la version produite (par exemple 1.6) évite qu’une mise à jour du générateur casse l’ingestion.

Pourquoi faut-il un SBOM du code et un SBOM de l’image ?

Parce que le lockfile et l’image ne contiennent pas les mêmes choses. Le lockfile décrit les dépendances du langage, alors que l’image livrée contient aussi une distribution (paquets apk ou Debian), un runtime, parfois des outils de construction oubliés. Un audit fondé sur le seul lockfile ne voit rien de cette couche.

InventaireSource analyséeOutilCe qu’il voit
Codearbre de sources et lockfilecdxgendépendances npm directes et transitives
Imageimage construite (ou archive)syftpaquets système, runtime, node_modules réellement copiés

Le cas est concret dans saas-core : la CI était verte alors que l’image admin portait 21 vulnérabilités critiques, parce que pnpm audit et OSV-Scanner ne lisent que le lockfile, jamais les paquets système des images. C’est ce constat qui a motivé le scan des images construites.

Comment générer un SBOM du code avec cdxgen ?

cdxgen analyse un dépôt et produit un SBOM CycloneDX à partir des manifestes et lockfiles qu’il reconnaît. Pour un monorepo pnpm, on lui indique le type de projet, la récursion et la version de spécification :

npx --yes @cyclonedx/cdxgen@11 -r . -t pnpm -o sbom-app.cdx.json --spec-version 1.6

-r parcourt le dépôt récursivement, -t pnpm restreint l’analyse à l’écosystème pnpm, -o désigne le fichier de sortie. Lancé après un pnpm install --frozen-lockfile, le résultat décrit exactement le lockfile commité.

Comment générer un SBOM d’image avec syft ?

syft inventorie le contenu d’une image de conteneur, couche système comprise, et sait l’écrire en CycloneDX. Il lit une image depuis un registre, depuis le démon Docker, ou depuis une archive produite par docker save. L’archive a l’avantage de ne dépendre d’aucune socket : elle fonctionne aussi avec un Docker en mode rootless.

docker save mon-image:abc1234 -o api.tar
syft docker-archive:api.tar -q -o cyclonedx-json@1.6=sbom-api.cdx.json

La syntaxe -o format=fichier écrit la sortie dans un fichier ; le suffixe @1.6 fixe la version de spécification. Sans format explicite, syft peut produire une version que Dependency-Track rejette (Unrecognized specVersion 1.7, panne relevée dans la documentation de saas-core).

Comment scanner une image à partir de son SBOM avec grype ?

grype compare un SBOM, ou directement une image, à sa base de vulnérabilités et rend la liste des correspondances, avec la sévérité et l’état du correctif. Scanner le SBOM plutôt que l’image garantit que l’inventaire archivé et l’analyse portent sur la même chose.

grype sbom:sbom-api.cdx.json -q -o json=grype-api.json

La sortie JSON indique pour chaque correspondance si l’amont a publié une version corrigée (fix.state). C’est l’information qui permet une règle de blocage raisonnable : bloquer ce qui peut être corrigé par une montée de version, lire le reste sans bloquer.

Que faire du SBOM après la CI ?

Un SBOM sert à trois usages : prouver ce qui a été livré, réévaluer les composants dans le temps, et le remettre à un client qui le demande. Archiver le SBOM comme artefact de CI couvre le premier usage, mais un fichier archivé ne dit rien le lendemain d’une divulgation.

La réévaluation continue demande un outil qui conserve les SBOM par version et les recompare chaque jour aux bases de vulnérabilités, comme OWASP Dependency-Track, décrit dans l’article consacré à Dependency-Track. La provenance de l’image elle-même relève de la signature, traitée dans l’article sur Cosign et SLSA.

Le contexte réglementaire pèse aussi. La documentation de saas-core cite le Cyber Resilience Act européen comme l’un des déclencheurs de ce chantier, avec une obligation de SBOM attendue à partir de 2027 pour les produits comportant des éléments numériques ; elle classe son applicabilité au socle comme « à évaluer ».

Comment saas-core l’intègre

saas-core produit trois familles de SBOM CycloneDX 1.6, toutes en CI, et applique une règle de blocage sur les images construites.

  • Code. .github/workflows/sbom.yml, à chaque push sur main : pnpm install --frozen-lockfile, puis cdxgen avec la commande ci-dessus. Le résultat est archivé en artefact sbom-app.
  • Images amont. Le même workflow inventorie PostgreSQL et Redis avec anchore/sbom-action (syft), au format cyclonedx-json. Limite documentée : ces deux images sont inventoriées par étiquette, alors que l’environnement d’essai les déploie par empreinte.
  • Images applicatives. Le job images de .github/workflows/ci.yml construit les cibles api, worker et admin à chaque PR et à chaque push sur main, sans les pousser, puis lance scripts/sbom/scan-images.mjs. Ce script fait docker save, produit le SBOM avec syft, le scanne avec grype, et écrit sbom-<nom>.cdx.json et grype-<nom>.json, archivés en artefact sbom-images-<sha>. syft et grype tournent en conteneur, épinglés par version et par empreinte dans scripts/sbom/tools.mjs (syft v1.52.0, grype v0.119.0).
  • Règle de blocage. scripts/sbom/image-scan-policy.mjs fait échouer le job pour toute vulnérabilité critique dont l’amont a publié un correctif, sauf exception de image-scan-exceptions.json, qui exige un identifiant, une raison et une date d’échéance. Les vulnérabilités élevées et les critiques sans correctif sont comptées dans le résumé du job, sans bloquer.
  • Rescan hebdomadaire. .github/workflows/image-scan.yml reconstruit les trois images de main chaque lundi à 05:00 UTC et relance le même script, avec la base de vulnérabilités du jour. Il ne notifie personne : son échec se lit dans l’onglet Actions.
  • Après la CI. Les SBOM d’images servent ensuite d’entrée à un Dependency-Track local, alimenté par pnpm sbom:sync, hors du chemin de livraison.

Les images api et worker sont aussi vérifiées dans le même job : elles ne doivent embarquer ni npm, ni npx, ni corepack, ni yarn, dont les dépendances remontaient au scan.

En résumé

Un SBOM du code et un SBOM de l’image répondent à deux questions différentes, et seul le second voit la couche système. Fixer la version CycloneDX, scanner le SBOM plutôt que l’image, et bloquer sur ce qui est corrigeable rend le dispositif tenable au quotidien.

Ce qu’inclut le kit saas-core, workflows de SBOM et de scan compris, est détaillé sur la page des tarifs.