Aller au contenu principal

Dépendances transitives

Un paquet vulnérable n’est souvent pas un paquet que vous avez déclaré. Une autre dépendance l’importe, et c’est elle qui choisit sa version. Il est impossible de le mettre à jour là où il est déclaré, puisqu’il n’est déclaré nulle part dans votre dépôt. Les correctifs de sécurité épinglent donc un tel paquet depuis la racine de votre projet, avec le mécanisme que chaque écosystème prévoit à cet effet.

ÉcosystèmeComment le paquet est épingléComment le demander
npm (package-lock.json)Une entrée overrides dans package.json, et les entrées du paquet dans package-lock.json passées à la version corrigéeOuvrir une demande de correctif sur le résultat, comme pour tout autre. Lorsque Renovate hébergé ne trouve rien à mettre à jour, fremforge l’épingle en une dizaine de minutes
pnpm (pnpm-lock.yaml)Une entrée overrides dans pnpm-workspace.yaml (ou pnpm.overrides dans package.json pour pnpm 9 et antérieur), et le verrouillage mis en concordanceComme pour npm
Yarn 1 (yarn.lock)Une entrée resolutions dans package.json, et l’entrée de yarn.lock déplacéeComme pour npm. Yarn 2 et versions ultérieures sont refusés
PyPI (uv.lock, poetry.lock)L’entrée de verrouillage du paquet passée à la version corrigée, sans épinglage dans pyproject.tomlComme pour npm
Maven (pom.xml)Une entrée <dependencyManagement> dans le pom.xml dont provient le résultatOuvrir une demande de correctif, comme pour npm
Maven (vu dans votre SBOM)Une entrée <dependencyManagement> dans le pom.xml racineÉpingler comme dépendance transitive sur le résultat, ou POST /orgs/{org}/dependency-updates/security-fixes/transitive-requests
Gradle (gradle.lockfile)Une entrée constraints dans le fichier de build, et la ligne du fichier de verrouillage déplacéeComme pour npm
GoRien de plus : go.mod liste chaque module utilisé par votre build, marqué // indirect, et le correctif relève cette ligneOuvrir une demande de correctif

Il s’agit d’une pull request de correctif de sécurité ordinaire, et le même âge de version s’applique : une version corrigée plus récente que l’Âge de la version (jours) de votre organisation (3 par défaut) n’est pas encore proposée, sauf si elle corrige une vulnérabilité connue comme exploitée. Comme aucune exécution de Renovate ne vérifie un épinglage transitif, fremforge lit lui-même la date de publication, via votre proxy de dépendances : le champ time du registre npm, l’heure de téléversement des fichiers de la version corrigée sur PyPI, et pour Maven et Gradle l’heure que Maven Central indique pour le pom. Une version corrigée dont la date de publication ne peut pas être lue n’est pas proposée.

Elle a aussi la même branche (security-fix/<package>-<version>), la même étiquette (security-fix), les mêmes limites et le même paramètre de fusion que toutes les autres, et le même choix Quand les correctifs sont fusionnés s’applique. La pull request indique que le paquet est transitif et comment il a été épinglé.

npm

La pull request modifie deux fichiers et rien d’autre :

"overrides": {
  "lodash": "4.18.0"
}

Ceci est ajouté à package.json. Chaque entrée de package-lock.json du paquet à une version vulnérable passe à la version corrigée : la version et l’URL de l’archive changent, ainsi que l’empreinte d’intégrité lorsque l’entrée en avait une. L’URL conserve le registre de votre fichier de verrouillage.

fremforge n’exécute pas npm install, ni quoi que ce soit d’autre provenant de votre dépôt. Il écrit les entrées de verrouillage à partir des métadonnées du registre, lues via le proxy de dépendances de votre organisation, de sorte que vos politiques de paquets s’appliquent aussi à la version corrigée. Il ne les écrit que lorsque c’est tout ce qu’une résolution modifierait :

  • La version corrigée déclare exactement les mêmes dépendances que la version qu’elle remplace. Sinon, la demande est refusée avec transitive_dependencies_changed, car l’arbre changerait au-delà de ce paquet.
  • Chaque copie du paquet dans le verrouillage est à une version vulnérable. Un override déplace toutes les copies ; une copie à une autre version, non concernée, fait donc refuser la demande avec override_conflict.
  • package-lock.json est en lockfileVersion 2 ou 3.

npm ci accepte le résultat. Supprimez l’override dès que les paquets qui importent le paquet exigent eux-mêmes la version corrigée.

pnpm

pnpm enregistre aussi les overrides dans le fichier de verrouillage, et refuse un verrouillage dont la section overrides: diffère de la configuration. La pull request modifie donc les deux :

  • l’override, dans pnpm-workspace.yaml (créé lorsque le dépôt n’en a pas). Il va plutôt dans pnpm.overrides de package.json lorsque packageManager désigne pnpm 9 ou antérieur, ou, en l’absence de packageManager, lorsque le projet y conserve ses overrides et n’a pas de pnpm-workspace.yaml ;
  • pnpm-lock.yaml : sa section overrides:, les entrées packages et snapshots du paquet avec l’intégrité issue du registre, et chaque référence à l’ancienne version.

pnpm install --frozen-lockfile accepte le résultat. Seul lockfileVersion: '9.0' est pris en charge ; un paquet verrouillé avec un suffixe de dépendance pair tel que foo@1.0.0(react@18.3.1) est refusé avec lock_entry_unsupported.

Yarn

Pour Yarn 1, la pull request ajoute une entrée resolutions à package.json et déplace l’entrée du paquet dans yarn.lock : sa version, son URL resolved (sur le registre de votre fichier de verrouillage, avec la somme de contrôle de l’archive) et son integrity. yarn install --frozen-lockfile accepte le résultat.

Yarn 2 et versions ultérieures (un yarn.lock avec __metadata) sont refusés avec yarn_berry_checksum. Leur verrouillage enregistre une somme de contrôle de l’archive que Yarn construit à partir du paquet, que seul Yarn peut calculer. Ajoutez vous-même une entrée resolutions et exécutez yarn install.

Les mêmes règles que pour npm s’appliquent à pnpm et Yarn : la version corrigée doit déclarer les mêmes dépendances, et chaque copie du paquet dans le verrouillage doit être à une version vulnérable.

Python

Pour uv.lock et poetry.lock, la pull request passe l’entrée de verrouillage du paquet à la version corrigée et n’ajoute rien à pyproject.toml. Un paquet que pyproject.toml déclare est refusé avec already_declared, car Ouvrir une demande de correctif le met à jour à cet endroit. La nouvelle entrée porte les fichiers de la version corrigée avec leurs empreintes, lus depuis le proxy de dépendances PyPI de votre organisation :

  • uv : le sdist et les wheels, avec l’URL, l’empreinte, la taille et l’heure de téléversement lorsque votre verrouillage les enregistre. Les URL conservent l’index de votre verrouillage : l’URL propre du proxy de dépendances si vous verrouillez à partir de lui, ou files.pythonhosted.org si vous verrouillez à partir de PyPI. Un verrouillage issu de tout autre index est refusé avec lock_entry_unsupported.
  • Poetry : la version, la description, python-versions et files. Le content-hash de Poetry ne couvre que pyproject.toml ; il ne change donc pas.

La règle est la même que pour npm, appliquée aux métadonnées de Python : la version corrigée doit avoir les mêmes Requires-Dist et Requires-Python que la version qu’elle remplace, lus dans les métadonnées de sa wheel. Sinon, la demande est refusée avec transitive_dependencies_changed. uv sync --locked et poetry install acceptent le résultat. Le paquet reste à la version corrigée jusqu’à ce qu’un uv lock ou poetry lock ultérieur refasse la résolution.

Maven

L’analyse des dépendances résout chaque pom.xml ; un paquet vulnérable que votre build importe est donc un résultat sur ce pom.xml. Ouvrir une demande de correctif fonctionne alors comme pour npm : lorsque Renovate hébergé ne trouve rien à mettre à jour, parce que le pom ne nomme pas le paquet, fremforge l’épingle dans ce pom.xml en une dizaine de minutes. Un paquet visible uniquement dans un SBOM que vous téléversez, par exemple produit par cyclonedx-maven-plugin (voir chemins de dépendance), affiche plutôt Épingler comme dépendance transitive dans la colonne Correctif, et est épinglé dans le pom.xml racine. Dans les deux cas, la pull request ajoute :

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.18.11</version>
    </dependency>
  </dependencies>
</dependencyManagement>

Cela fixe la version du paquet partout où il apparaît dans le build. Si le pom le gère déjà à une version littérale inférieure, cette version est relevée sur place. La demande est refusée lorsque :

  • le pom déclare le paquet comme l’une de ses propres dépendances (already_declared). Ouvrir une demande de correctif le met à jour à cet endroit ;
  • la version gérée est lue depuis une propriété, comme ${jackson.version}, que d’autres entrées peuvent aussi lire (managed_by_property) ;
  • il est déjà géré à la version corrigée ou supérieure (already_managed).

Vérifiez que les dépendances qui importent le paquet fonctionnent avec la nouvelle version, car une bibliothèque est généralement testée avec la version qu’elle a demandée. Supprimez l’entrée dès qu’elles exigent elles-mêmes la version corrigée.

Gradle

Pour un build Gradle avec un fichier de verrouillage (gradle.lockfile), la pull request ajoute une contrainte au bloc dependencies du fichier de build situé à côté du fichier de verrouillage (build.gradle.kts ou build.gradle) :

dependencies {
    constraints {
        implementation("org.yaml:snakeyaml:2.0") {
            because("fremforge security fix: pins a transitive dependency to a version that fixes its open advisories")
        }
    }
}

Elle déplace aussi la ligne du paquet dans gradle.lockfile, de sorte que le verrouillage et le build concordent et que Gradle accepte le verrouillage en mode strict. Comme pour Maven, le pom de la version corrigée doit déclarer les mêmes dépendances et le même parent que celui qu’elle remplace. La demande est refusée lorsque :

  • le fichier de build nomme déjà le paquet (already_declared) ;
  • le build vérifie les dépendances avec gradle/verification-metadata.xml (gradle_verification_metadata). La nouvelle version a besoin de sommes de contrôle dans ce fichier, que Gradle calcule ; exécutez vous-même ./gradlew --write-verification-metadata sha256 ;
  • le paquet est verrouillé dans une configuration qu’une contrainte implementation n’atteint pas, comme le classpath propre d’un plugin (gradle_configuration_unsupported) ;
  • le fichier de build n’a pas de bloc dependencies { de premier niveau (build_file_unsupported).

fremforge n’exécute pas Gradle. Un build Gradle de la pull request dans votre CI vérifie le verrouillage par rapport au build et échoue s’ils divergent.

Quand aucune pull request n’est ouverte

La demande affiche alors la raison. Via l’API, reason vaut transitive_<why> et detail l’explique :

RaisonSignification
transitive_already_declaredLe paquet est une dépendance directe. Ouvrir une demande de correctif le met à jour là où il est déclaré
transitive_transitive_dependencies_changedLa version corrigée déclare d’autres dépendances (pour Python, aussi un autre Requires-Python), si bien que le verrouillage nécessite une résolution complète
transitive_override_conflictUne autre copie du paquet, à une version non concernée, serait aussi déplacée, ou un override, une résolution ou une contrainte l’épingle déjà différemment
transitive_yarn_berry_checksumYarn 2 ou ultérieur : son verrouillage nécessite une somme de contrôle que seul Yarn peut calculer
transitive_gradle_verification_metadataGradle : le build vérifie les dépendances, et la nouvelle version a besoin de sommes de contrôle que Gradle calcule
transitive_gradle_configuration_unsupportedGradle : le paquet est verrouillé dans une configuration qu’une contrainte implementation n’atteint pas
transitive_build_file_unsupportedGradle : le fichier de build n’a pas de bloc dependencies { de premier niveau
transitive_lock_entry_unsupportedL’entrée de verrouillage a une forme que fremforge ne modifie pas, comme un suffixe pair pnpm, plusieurs versions verrouillées, ou un verrouillage uv issu d’un index qu’il ne sert pas
transitive_managed_by_propertyMaven : la version gérée provient d’une propriété
transitive_already_managedMaven : déjà géré à la version corrigée ou supérieure
transitive_lock_version_unsupportednpm : lockfileVersion 1. pnpm : une version de fichier de verrouillage autre que 9.0
transitive_manifest_not_supportedUn fichier de verrouillage absent de la liste ci-dessus, comme bun.lock, pdm.lock ou Pipfile.lock
transitive_target_metadata_missingLe proxy de dépendances n’a fourni aucune métadonnée pour la version corrigée, par exemple parce qu’une politique de paquets la refuse
transitive_manifest_missing, transitive_lock_missingLe manifeste ou le fichier de verrouillage ne se trouve pas au chemin attendu sur la branche par défaut
transitive_release_age_pendingLa version corrigée est plus récente que votre âge de version ; refaites la demande après la date indiquée dans le détail
transitive_release_age_unknownLe proxy de dépendances n’a fourni aucune date de publication pour la version corrigée
transitive_branch_existsLa branche security-fix/<package>-<version> existe déjà
transitive_repo_not_writableLe dépôt est archivé ou est un miroir

Pour une demande passée par Ouvrir une demande de correctif (tous les écosystèmes sauf Maven), la demande conserve Aucune PR : rien à modifier et le détail indique la raison.

API

npm, pnpm, Yarn, Python et Gradle n’ont besoin de rien d’autre que POST /orgs/{org}/dependency-updates/security-fixes/requests. Pour Maven :

curl -sS -X POST "https://frem.sh/_app/api/v1/orgs/acme/dependency-updates/security-fixes/transitive-requests" \
  -H "Authorization: Bearer $FREMFORGE_TOKEN" -H "Content-Type: application/json" \
  -d '{"finding_id": "<dependency finding id>"}'

La réponse est 201 avec la demande et son issue : pr_opened avec pr_number et pr_url, ou rejected avec reason et detail. Un refus avant qu’une demande existe est un problème de type security-fix-<reason>, par exemple security-fix-not-transitive. La portée est dependency-updates:write. La demande est ensuite listée par GET …/security-fixes/requests comme toutes les autres.