Naar de hoofdinhoud

Transitieve afhankelijkheden

Een kwetsbaar pakket is vaak niet een pakket dat je zelf hebt gedeclareerd. Een andere afhankelijkheid haalt het binnen, en die afhankelijkheid kiest de versie. Het verhogen waar het gedeclareerd is, kan niet, want het is nergens in je repository gedeclareerd. Beveiligingsfixes zetten zo’n pakket daarom vast vanuit de root van je project, met het mechanisme dat elk ecosysteem daarvoor biedt.

EcosysteemHoe het pakket wordt vastgezetHoe je erom vraagt
npm (package-lock.json)Een overrides-vermelding in package.json, en de vermeldingen van het pakket in package-lock.json verplaatst naar de gefixte versieFix-PR openen bij de bevinding, net als bij elke andere. Vindt gehoste Renovate niets om te verhogen, dan zet fremforge het binnen ongeveer tien minuten vast
pnpm (pnpm-lock.yaml)Een overrides-vermelding in pnpm-workspace.yaml (of pnpm.overrides in package.json voor pnpm 9 en ouder), en de lock daarop aangepastNet als bij npm
Yarn 1 (yarn.lock)Een resolutions-vermelding in package.json, en de vermelding in yarn.lock verplaatstNet als bij npm. Yarn 2 en later worden geweigerd
PyPI (uv.lock, poetry.lock)De lock-vermelding van het pakket verplaatst naar de gefixte versie, zonder vastzetting in pyproject.tomlNet als bij npm
Maven (pom.xml)Een <dependencyManagement>-vermelding in de pom.xml waar de bevinding uit komtFix-PR openen, net als bij npm
Maven (gezien in je SBOM)Een <dependencyManagement>-vermelding in de root-pom.xmlVastzetten als transitief bij de bevinding, of POST /orgs/{org}/dependency-updates/security-fixes/transitive-requests
Gradle (gradle.lockfile)Een constraints-vermelding in het buildbestand, en de regel in de lockfile verplaatstNet als bij npm
GoNiets extra’s: go.mod somt elke module op die je build gebruikt, gemarkeerd met // indirect, en de fix verhoogt die regelFix-PR openen

Het is een gewone pull request voor een beveiligingsfix, en dezelfde releaseleeftijd geldt: een gefixte versie die jonger is dan de Releaseleeftijd (dagen) van je organisatie (standaard 3) wordt nog niet voorgesteld, tenzij ze een bekend misbruikte kwetsbaarheid oplost. Omdat geen Renovate-run een transitieve vastzetting controleert, leest fremforge de publicatietijd zelf, via je dependency proxy: time uit het npm-register, de uploadtijd van de bestanden van de gefixte versie op PyPI, en voor Maven en Gradle de tijd die Maven Central voor de pom geeft. Een gefixte versie waarvan de publicatietijd niet te lezen is, wordt niet voorgesteld.

Hij heeft ook dezelfde branch (security-fix/<package>-<version>), hetzelfde label (security-fix), dezelfde limieten en dezelfde merge-instelling als elke andere, en dezelfde keuze onder Wanneer fixes worden gemerged geldt. De pull request vermeldt dat het pakket transitief is en hoe het is vastgezet.

npm

De pull request wijzigt twee bestanden en verder niets:

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

Dat wordt toegevoegd aan package.json. Elke vermelding van het pakket in package-lock.json met een kwetsbare versie wordt verplaatst naar de gefixte versie: de versie en de tarball-URL veranderen, en de integriteitshash verandert waar de vermelding er een had. De URL houdt het register van je lockfile.

fremforge voert geen npm install uit, en ook niets anders uit je repository. Het schrijft de lock-vermeldingen op basis van de metadata van het register, gelezen via de dependency proxy van je organisatie, zodat je pakketbeleid ook voor de gefixte versie geldt. Het schrijft ze alleen als dat het enige is wat een resolve zou veranderen:

  • De gefixte versie declareert precies dezelfde afhankelijkheden als de versie die ze vervangt. Anders wordt de aanvraag geweigerd met transitive_dependencies_changed, omdat de boom verder zou veranderen dan dit ene pakket.
  • Elke kopie van het pakket in de lock heeft een kwetsbare versie. Een override verplaatst elke kopie, dus een kopie met een andere, niet-getroffen versie weigert de aanvraag met override_conflict.
  • package-lock.json heeft lockfileVersion 2 of 3.

npm ci accepteert het resultaat. Verwijder de override zodra de pakketten die het pakket binnenhalen zelf de gefixte versie vereisen.

pnpm

pnpm legt overrides ook in de lockfile vast, en weigert een lock waarvan de sectie overrides: afwijkt van de configuratie. De pull request wijzigt daarom beide:

  • de override, in pnpm-workspace.yaml (aangemaakt als de repository er geen heeft). Hij komt in plaats daarvan in pnpm.overrides in package.json als packageManager pnpm 9 of ouder noemt, of, zonder packageManager, als het project zijn overrides daar bijhoudt en geen pnpm-workspace.yaml heeft;
  • pnpm-lock.yaml: de sectie overrides:, de vermeldingen van het pakket onder packages en snapshots met de integriteit uit het register, en elke verwijzing naar de oude versie.

pnpm install --frozen-lockfile accepteert het resultaat. Alleen lockfileVersion: '9.0' wordt ondersteund; een pakket dat is gelockt met een peer-dependency-suffix zoals foo@1.0.0(react@18.3.1) wordt geweigerd met lock_entry_unsupported.

Yarn

Voor Yarn 1 voegt de pull request een resolutions-vermelding toe aan package.json en verplaatst hij de vermelding van het pakket in yarn.lock: de versie, de resolved-URL (op het register van je lockfile, met de checksum van de tarball) en de integrity. yarn install --frozen-lockfile accepteert het resultaat.

Yarn 2 en later (een yarn.lock met __metadata) worden geweigerd met yarn_berry_checksum. Hun lock legt een checksum vast van het archief dat Yarn uit het pakket bouwt, en die kan alleen Yarn zelf berekenen. Voeg zelf een resolutions-vermelding toe en voer yarn install uit.

Voor pnpm en Yarn gelden dezelfde regels als voor npm: de gefixte versie moet dezelfde afhankelijkheden declareren, en elke kopie van het pakket in de lock moet een kwetsbare versie hebben.

Python

Voor uv.lock en poetry.lock verplaatst de pull request de lock-vermelding van het pakket naar de gefixte versie en voegt hij niets toe aan pyproject.toml. Een pakket dat pyproject.toml declareert, wordt geweigerd met already_declared, omdat Fix-PR openen het daar verhoogt. De nieuwe vermelding bevat de bestanden van de gefixte versie met hun hashes, gelezen uit de PyPI-dependency proxy van je organisatie:

  • uv: de sdist en wheels, met URL, hash, grootte en uploadtijd waar je lock die vastlegt. De URL’s houden de index van je lock: de eigen URL van de dependency proxy als je daartegen lockt, of files.pythonhosted.org als je tegen PyPI lockt. Een lock van een andere index wordt geweigerd met lock_entry_unsupported.
  • Poetry: de versie, de beschrijving, python-versions en files. De content-hash van Poetry dekt alleen pyproject.toml, dus die verandert niet.

De regel is dezelfde als voor npm, toegepast op de metadata van Python: de gefixte versie moet dezelfde Requires-Dist en Requires-Python hebben als de versie die ze vervangt, gelezen uit de metadata van haar wheel. Anders wordt de aanvraag geweigerd met transitive_dependencies_changed. uv sync --locked en poetry install accepteren het resultaat. Het pakket blijft op de gefixte versie totdat een latere uv lock of poetry lock opnieuw resolvet.

Maven

Het scannen van afhankelijkheden resolvet elke pom.xml, dus een kwetsbaar pakket dat je build binnenhaalt, is een bevinding op die pom.xml. Fix-PR openen werkt daar net als bij npm: vindt gehoste Renovate niets om te verhogen, omdat de pom het pakket niet noemt, dan zet fremforge het binnen ongeveer tien minuten vast in die pom.xml. Een pakket dat alleen voorkomt in een SBOM die je uploadt, zoals een van cyclonedx-maven-plugin (zie afhankelijkheidspaden), toont in de kolom Fix in plaats daarvan Vastzetten als transitief, en wordt vastgezet in de root-pom.xml. In beide gevallen voegt de pull request toe:

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

Dat stelt de versie van het pakket in, waar het ook in de build voorkomt. Beheert de pom het al op een lagere letterlijke versie, dan wordt die versie ter plekke verhoogd. De aanvraag wordt geweigerd als:

  • de pom het pakket als eigen afhankelijkheid declareert (already_declared). Fix-PR openen verhoogt het daar;
  • de beheerde versie uit een property wordt gelezen, zoals ${jackson.version}, die ook door andere vermeldingen kan worden gelezen (managed_by_property);
  • het al op de gefixte versie of hoger wordt beheerd (already_managed).

Controleer of de afhankelijkheden die het pakket binnenhalen met de nieuwe versie werken, want een bibliotheek is meestal getest tegen de versie waar ze om vroeg. Verwijder de vermelding zodra ze zelf de gefixte versie vereisen.

Gradle

Voor een Gradle-build met een lockfile (gradle.lockfile) voegt de pull request een constraint toe aan het dependencies-blok van het buildbestand naast de lockfile (build.gradle.kts of 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")
        }
    }
}

Hij verplaatst ook de regel van het pakket in gradle.lockfile, zodat lock en build overeenkomen en Gradle de lock in strikte modus accepteert. Net als bij Maven moet de pom van de gefixte versie dezelfde afhankelijkheden en parent declareren als de versie die ze vervangt. De aanvraag wordt geweigerd als:

  • het buildbestand het pakket al noemt (already_declared);
  • de build afhankelijkheden verifieert met gradle/verification-metadata.xml (gradle_verification_metadata). De nieuwe versie heeft checksums in dat bestand nodig, die Gradle berekent; voer zelf ./gradlew --write-verification-metadata sha256 uit;
  • het pakket is gelockt in een configuratie die een implementation-constraint niet bereikt, zoals het eigen classpath van een plugin (gradle_configuration_unsupported);
  • het buildbestand geen dependencies {-blok op het hoogste niveau heeft (build_file_unsupported).

fremforge voert Gradle niet uit. Een Gradle-build van de pull request in je CI controleert de lock tegen de build en mislukt als ze niet overeenkomen.

Als er geen pull request wordt geopend

De aanvraag toont dan de reden. Via de API is reason gelijk aan transitive_<why> en licht detail het toe:

RedenBetekenis
transitive_already_declaredHet pakket is een directe afhankelijkheid. Fix-PR openen verhoogt het waar het gedeclareerd is
transitive_transitive_dependencies_changedDe gefixte versie declareert andere afhankelijkheden (voor Python ook een andere Requires-Python), dus de lock heeft een volledige resolve nodig
transitive_override_conflictEen andere kopie van het pakket, met een niet-getroffen versie, zou ook verplaatsen, of een override, resolution of constraint zet het al anders vast
transitive_yarn_berry_checksumYarn 2 of later: de lock heeft een checksum nodig die alleen Yarn kan berekenen
transitive_gradle_verification_metadataGradle: de build verifieert afhankelijkheden, en de nieuwe versie heeft checksums nodig die Gradle berekent
transitive_gradle_configuration_unsupportedGradle: het pakket is gelockt in een configuratie die een implementation-constraint niet bereikt
transitive_build_file_unsupportedGradle: het buildbestand heeft geen dependencies {-blok op het hoogste niveau
transitive_lock_entry_unsupportedDe lock-vermelding heeft een vorm die fremforge niet bewerkt, zoals een pnpm-peersuffix, meerdere gelockte versies of een uv-lock van een index die het niet bedient
transitive_managed_by_propertyMaven: de beheerde versie komt uit een property
transitive_already_managedMaven: al beheerd op de gefixte versie of hoger
transitive_lock_version_unsupportednpm: lockfileVersion 1. pnpm: een andere lockfileversie dan 9.0
transitive_manifest_not_supportedEen lockfile die hierboven niet staat, zoals bun.lock, pdm.lock of Pipfile.lock
transitive_target_metadata_missingDe dependency proxy gaf geen metadata voor de gefixte versie, bijvoorbeeld omdat een pakketbeleid haar weigert
transitive_manifest_missing, transitive_lock_missingHet manifest of de lockfile staat niet op het verwachte pad op de standaardbranch
transitive_release_age_pendingDe gefixte versie is jonger dan je releaseleeftijd; vraag het opnieuw aan na de datum die het detail noemt
transitive_release_age_unknownDe dependency proxy gaf geen publicatietijd voor de gefixte versie
transitive_branch_existsDe branch security-fix/<package>-<version> bestaat al
transitive_repo_not_writableDe repository is gearchiveerd of een spiegel

Voor een aanvraag via Fix-PR openen (elk ecosysteem behalve Maven) blijft de aanvraag op Geen PR: niets te wijzigen staan en noemt het detail de reden.

API

npm, pnpm, Yarn, Python en Gradle hebben niets nodig naast POST /orgs/{org}/dependency-updates/security-fixes/requests. Voor 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>"}'

Het antwoord is 201 met de aanvraag en de uitkomst ervan: pr_opened met pr_number en pr_url, of rejected met reason en detail. Een weigering voordat er een aanvraag bestaat, is een probleem van het type security-fix-<reason>, bijvoorbeeld security-fix-not-transitive. De scope is dependency-updates:write. De aanvraag wordt daarna net als elke andere vermeld door GET …/security-fixes/requests.