pnpm audit et OSV-Scanner lisent le lockfile et comparent chaque dépendance à une base d’avis de sécurité ; placés en CI, ils empêchent qu’une dépendance vulnérable connue entre sur la branche principale. Les deux ne voient pas les mêmes choses, ils ne bloquent pas au même seuil, et ils finiront tôt ou tard par rougir sur un commit qui n’a rien changé. Cet article décrit comment les configurer et comment traiter proprement un avis fraîchement divulgué.
Que vérifient pnpm audit et OSV-Scanner ?
Les deux outils vérifient les versions résolues du lockfile contre une base d’avis, mais pas la même base. pnpm audit interroge la base d’avis du registre npm. OSV-Scanner interroge OSV.dev, qui agrège plusieurs sources, dont les GitHub Security Advisories, et couvre de nombreux écosystèmes.
pnpm audit | OSV-Scanner | |
|---|---|---|
| Entrée | lockfile pnpm du projet | lockfile passé en argument (ici pnpm-lock.yaml) |
| Base d’avis | avis du registre npm | OSV.dev |
| Seuil | réglable par --audit-level | toute vulnérabilité trouvée, sauf exception configurée |
| Exceptions | pnpm.overrides pour corriger | fichier de configuration ([[IgnoredVulns]]) |
En pratique, les résultats diffèrent. Dans saas-core, OSV-Scanner a relevé des avis sur uuid et esbuild que pnpm audit n’avait pas signalés. Exécuter les deux n’est donc pas redondant.
Aucun des deux ne lit les paquets système d’une image de conteneur. Ce point relève du scan d’images, traité dans l’article sur les SBOM CycloneDX, cdxgen et syft.
Quel gate doit être bloquant ?
Un gate est utile s’il est bloquant et si son seuil est tenable. pnpm audit --audit-level=high sort en erreur seulement pour un avis de sévérité haute ou critique : les avis faibles et modérés restent affichés sans faire échouer le job. OSV-Scanner, lui, échoue dès qu’une vulnérabilité est trouvée, quelle que soit sa sévérité, sauf si elle est explicitement ignorée dans sa configuration.
Un gate informatif est vite oublié. Un gate bloquant au mauvais seuil est vite contourné. La combinaison « haute et critique » pour pnpm audit et « tout, avec exceptions écrites » pour OSV-Scanner donne une règle stricte, mais dont chaque dérogation laisse une trace.
# Extrait de .github/workflows/ci.yml (saas-core)
- name: dependency audit (high)
run: pnpm audit --audit-level=high
- name: OSV-Scanner (OSV.dev)
run: docker run --rm -v "${{ github.workspace }}:/src" ghcr.io/google/osv-scanner:v2.6.0@sha256:<empreinte> --lockfile=/src/pnpm-lock.yaml --config=/src/osv-scanner.toml
L’image d’OSV-Scanner est épinglée par version et par empreinte : sans cela, le code du gate changerait à chaque publication amont. La base d’avis, elle, reste celle du jour, puisque OSV.dev est interrogé en ligne.
Pourquoi le gate rougit-il sur un commit qui n’a rien changé ?
Parce qu’un avis a été publié entre deux runs. Le lockfile est identique, la base d’avis ne l’est plus. Le symptôme est un échec à l’étape d’audit sur un commit qui ne touche pas aux dépendances, souvent au pire moment : juste avant une livraison.
La tentation est de désactiver l’étape ou de relancer jusqu’au vert. Les deux reviennent à livrer une dépendance vulnérable connue. La bonne réponse est une remédiation courte, ciblée, dans un commit séparé.
Comment traiter un avis frais sans rapport avec le changement ?
Il faut d’abord qualifier l’avis, puis choisir la remédiation la plus petite, dans un ordre fixe.
- Qualifier. Lire l’identifiant (
GHSA-…,CVE-…), le paquet et la version corrigée dans le journal du job, puispnpm why <paquet>pour savoir si la dépendance est directe ou transitive, et par quel chemin elle arrive. - Correctif disponible, dépendance transitive. Ajouter un override pnpm ciblé par plage, qui ne touche que les versions vulnérables.
- Dépendance directe. Relever la version minimale dans le
package.jsondu paquet concerné, puis lancer ses tests. - Pas de correctif, ou chemin vulnérable inatteignable. Écrire une exception justifiée : une qualification VEX
not_affected, avec la raison. - Montée majeure nécessaire. Exception temporaire, puis un lot dédié pour la montée, jamais dans le même commit.
Un override bien écrit est borné des deux côtés :
{
"pnpm": {
"overrides": {
"js-yaml@>=4.0.0 <4.3.2": ">=4.3.2 <5",
"path-to-regexp@>=8.0.0 <8.4.0": ">=8.4.0"
}
}
}
Le sélecteur paquet@plage ne vise que les versions vulnérables. Écrire "js-yaml": ">=4.3.2" sans plage forcerait toutes les lignes majeures présentes dans l’arbre vers une même version, et casserait celles qui ne sont pas compatibles. Après pnpm install, le lockfile change : il doit être commité, puisque la CI installe en --frozen-lockfile.
Pourquoi une exception doit-elle être écrite, justifiée et datée ?
Parce qu’une exception sans raison est un ignore silencieux, et qu’une exception sans date devient permanente sans que personne ne l’ait décidé. Une exception est une assertion de sécurité : « cette vulnérabilité est présente, mais elle n’est pas exploitable ici, pour telle raison ». Elle doit pouvoir être relue, contestée et retirée.
OSV-Scanner accepte, pour chaque vulnérabilité ignorée, un identifiant, une raison et une date d’expiration optionnelle :
[[IgnoredVulns]]
id = "GHSA-xxxx-xxxx-xxxx"
ignoreUntil = 2026-12-31
reason = "Le chemin vulnérable est le serveur de développement, jamais exécuté en production."
Cette exception ne débloque que le gate. Pour qu’un tiers (client, auditeur) puisse vérifier la position, la même qualification s’écrit en VEX, par exemple au format OpenVEX : statut (not_affected, affected, fixed, under_investigation), justification normée (vulnerable_code_not_in_execute_path, component_not_present…) et texte d’impact. Un VEX daté et versionné dans le dépôt se relit à chaque montée du composant concerné ; quand la vulnérabilité disparaît, l’exception se retire.
Comment saas-core l’intègre
Les deux gates sont bloquants et vivent dans le job ci de .github/workflows/ci.yml : l’étape dependency audit (high) exécute pnpm audit --audit-level=high, l’étape OSV-Scanner (OSV.dev) exécute OSV-Scanner v2.6.0 en conteneur, épinglé par empreinte, sur pnpm-lock.yaml avec osv-scanner.toml. Ils tournent sur chaque PR et chaque push sur main. Le job livrer de .github/workflows/release-images.yml refuse ensuite toute étiquette dont les runs ci et e2e du commit ne sont pas verts : un avis non traité bloque donc aussi la mise en ligne.
- Corrections. Les correctifs transitifs passent par
pnpm.overridesdans lepackage.jsonracine, bornés par plage (path-to-regexp,js-yaml,uuid,esbuild,undici, entre autres). - Exceptions du gate.
osv-scanner.tomlexige un motif pour chaque[[IgnoredVulns]]. À la date de rédaction, il ne contient aucune exception active : son en-tête rappelle que ce qui est corrigé passe par les overrides et que seuls lesnot_affectedassumés y entrent. - VEX.
security/vex/saas-core.openvex.jsonest un document OpenVEX 0.2.0, daté, qui contient une déclaration :GHSA-67mh-4wv8-2f99surpkg:npm/esbuild,not_affected, justificationvulnerable_code_not_in_execute_path(le serveur de développement d’esbuild n’est jamais lancé, seules des builds sont livrées). À chaque étiquette,release-images.ymll’attache à chaque image parcosign attest --type openvex. - Images. Les exceptions du scan d’images vivent à part, dans
image-scan-exceptions.json, oùid,reasonet une échéanceuntilsont obligatoires ; une exception échue fait de nouveau rougir le scan. - Procédure. Le skill
avis-frais(.claude/skills/avis-frais/SKILL.md) décrit la remédiation dans l’ordre ci-dessus : qualifier, override, montée minimale, VEX aux deux endroits (osv-scanner.tomletsecurity/vex/), prouver, puis poser une nouvelle étiquette plutôt que relancer la livraison de l’ancienne.
Une limite est documentée : aucun test ne vérifie que osv-scanner.toml et le document OpenVEX restent alignés. L’ensemble s’inscrit dans le pipeline décrit dans l’article sur la CI/CD GitHub Actions d’un SaaS.
En résumé
pnpm audit et OSV-Scanner se complètent et doivent être bloquants. Un avis frais se traite par la plus petite remédiation possible : override borné, montée minimale, ou exception VEX justifiée et datée, versionnée dans le dépôt.
Le contenu du kit saas-core, gates de dépendances et VEX compris, est détaillé sur la page des tarifs.