Skip to main content

Transitive dependencies

A vulnerable package is often not one you declared. Another dependency pulls it in, and that dependency chooses its version. Bumping it where it is declared is not possible, because it is declared nowhere in your repository. Security fixes pin such a package from the root of your project instead, with the mechanism each ecosystem provides for that.

EcosystemHow the package is pinnedHow you ask for it
npm (package-lock.json)An overrides entry in package.json, and the package’s entries in package-lock.json moved to the fixed versionOpen fix PR on the finding, as for any other. When hosted Renovate finds nothing to bump, fremforge pins it within about ten minutes
pnpm (pnpm-lock.yaml)An overrides entry in pnpm-workspace.yaml (or package.json pnpm.overrides for pnpm 9 and older), and the lock moved to matchAs for npm
Yarn 1 (yarn.lock)A resolutions entry in package.json, and the yarn.lock entry movedAs for npm. Yarn 2 and later are refused
PyPI (uv.lock, poetry.lock)The package’s lock entry moved to the fixed version, with no pin in pyproject.tomlAs for npm
Maven (pom.xml)A <dependencyManagement> entry in the pom.xml the finding came fromOpen fix PR, as for npm
Maven (seen in your SBOM)A <dependencyManagement> entry in the root pom.xmlPin as transitive on the finding, or POST /orgs/{org}/dependency-updates/security-fixes/transitive-requests
Gradle (gradle.lockfile)A constraints entry in the build file, and the lockfile line movedAs for npm
GoNothing extra: go.mod lists every module your build uses, marked // indirect, and the fix raises that lineOpen fix PR

It is an ordinary security fix pull request, and the same release age applies: a fixed version younger than your organisation’s Release age (days) (3 by default) is not proposed yet, unless it fixes a known-exploited vulnerability. Because no Renovate run checks a transitive pin, fremforge reads the publish time itself, through your dependency proxy: npm’s registry time, PyPI’s upload time of the fixed version’s files, and for Maven and Gradle the time Maven Central gives the pom. A fixed version whose publish time cannot be read is not proposed.

It also has the same branch (security-fix/<package>-<version>), label (security-fix), limits and merge setting as every other, and the same When fixes merge choice applies. The pull request says that the package is transitive and how it was pinned.

npm

The pull request changes two files and nothing else:

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

That is added to package.json. Every package-lock.json entry of the package at a vulnerable version is moved to the fixed version, with the version and the tarball URL changed, and the integrity hash changed where the entry had one. The URL keeps your lockfile’s registry.

fremforge does not run npm install, or anything else from your repository. It writes the lock entries from the registry’s metadata, read through your organisation’s dependency proxy, so your package policies apply to the fixed version too. It writes them only when that is all a resolve would change:

  • The fixed version declares exactly the dependencies of the version it replaces. Otherwise the request is refused with transitive_dependencies_changed, because the tree would change beyond this package.
  • Every copy of the package in the lock is at a vulnerable version. An override moves every copy, so a copy at another, unaffected version refuses the request with override_conflict.
  • package-lock.json is lockfileVersion 2 or 3.

npm ci accepts the result. Remove the override once the packages that pull the package in require the fixed version themselves.

pnpm

pnpm records overrides in the lockfile as well, and refuses a lock whose overrides: section differs from the configuration. The pull request therefore changes both:

  • the override, in pnpm-workspace.yaml (created when the repository has none). It goes in package.json pnpm.overrides instead when packageManager names pnpm 9 or older, or, with no packageManager, when the project keeps its overrides there and has no pnpm-workspace.yaml;
  • pnpm-lock.yaml: its overrides: section, the package’s packages and snapshots entries with the integrity from the registry, and every reference to the old version.

pnpm install --frozen-lockfile accepts the result. Only lockfileVersion: '9.0' is supported; a package locked with a peer-dependency suffix such as foo@1.0.0(react@18.3.1) is refused with lock_entry_unsupported.

Yarn

For Yarn 1, the pull request adds a resolutions entry to package.json and moves the package’s yarn.lock entry: its version, its resolved URL (on your lockfile’s registry, with the tarball’s checksum) and its integrity. yarn install --frozen-lockfile accepts the result.

Yarn 2 and later (a yarn.lock with __metadata) are refused with yarn_berry_checksum. Their lock records a checksum of the archive Yarn builds from the package, which only Yarn itself can compute. Add a resolutions entry yourself and run yarn install.

The same rules as for npm apply to pnpm and Yarn: the fixed version must declare the same dependencies, and every copy of the package in the lock must be at a vulnerable version.

Python

For uv.lock and poetry.lock, the pull request moves the package’s lock entry to the fixed version and adds nothing to pyproject.toml. A package that pyproject.toml declares is refused with already_declared, because Open fix PR bumps it there. The new entry carries the fixed version’s files with their hashes, read from your organisation’s PyPI dependency proxy:

  • uv: the sdist and wheels, with the URL, hash, size and upload time where your lock records them. The URLs keep your lock’s index: the dependency proxy’s own URL when you lock against it, or files.pythonhosted.org when you lock against PyPI. A lock from any other index is refused with lock_entry_unsupported.
  • Poetry: the version, the description, python-versions and files. Poetry’s content-hash covers pyproject.toml only, so it does not change.

The rule is the same as for npm, applied to Python’s metadata: the fixed version must have the same Requires-Dist and Requires-Python as the version it replaces, read from its wheel’s metadata. Otherwise the request is refused with transitive_dependencies_changed. uv sync --locked and poetry install accept the result. The package stays at the fixed version until a later uv lock or poetry lock resolves again.

Maven

Dependency scanning resolves each pom.xml, so a vulnerable package your build pulls in is a finding on that pom.xml. Open fix PR on it works as for npm: when hosted Renovate finds nothing to bump, because the pom does not name the package, fremforge pins it in that pom.xml within about ten minutes. A package seen only in an SBOM you upload, such as one from cyclonedx-maven-plugin (see dependency paths), shows Pin as transitive in the Fix column instead, and is pinned in the root pom.xml. Either way the pull request adds:

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

That sets the version of the package wherever it appears in the build. If the pom already manages it at a lower literal version, that version is raised in place. It is refused when:

  • the pom declares the package as a dependency of its own (already_declared). Open fix PR bumps it there;
  • the managed version is read from a property, such as ${jackson.version}, which other entries may also read (managed_by_property);
  • it is already managed at the fixed version or higher (already_managed).

Check that the dependencies that pull the package in work with the new version, because a library is usually tested against the version it asked for. Remove the entry once they require the fixed version themselves.

Gradle

For a Gradle build with a lockfile (gradle.lockfile), the pull request adds a constraint to the dependencies block of the build file next to the lockfile (build.gradle.kts or 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")
        }
    }
}

It also moves the package’s line in gradle.lockfile, so the lock and the build agree and Gradle accepts the lock in strict mode. As for Maven, the fixed version’s pom must declare the same dependencies and parent as the one it replaces. It is refused when:

  • the build file already names the package (already_declared);
  • the build verifies dependencies with gradle/verification-metadata.xml (gradle_verification_metadata). The new version needs checksums in that file, which Gradle computes; run ./gradlew --write-verification-metadata sha256 yourself;
  • the package is locked in a configuration an implementation constraint does not reach, such as a plugin’s own classpath (gradle_configuration_unsupported);
  • the build file has no top-level dependencies { block (build_file_unsupported).

fremforge does not run Gradle. A Gradle build of the pull request in your CI checks the lock against the build and fails if they disagree.

When no pull request is opened

The request then shows the reason. Through the API, reason is transitive_<why> and detail explains it:

ReasonMeaning
transitive_already_declaredThe package is a direct dependency. Open fix PR bumps it where it is declared
transitive_transitive_dependencies_changedThe fixed version declares other dependencies (for Python, also another Requires-Python), so the lock needs a full resolve
transitive_override_conflictAnother copy of the package, at an unaffected version, would move too, or an override, resolution or constraint already pins it differently
transitive_yarn_berry_checksumYarn 2 or later: its lock needs a checksum only Yarn can compute
transitive_gradle_verification_metadataGradle: the build verifies dependencies, and the new version needs checksums Gradle computes
transitive_gradle_configuration_unsupportedGradle: the package is locked in a configuration an implementation constraint does not reach
transitive_build_file_unsupportedGradle: the build file has no top-level dependencies { block
transitive_lock_entry_unsupportedThe lock entry has a form fremforge does not edit, such as a pnpm peer suffix, several locked versions, or a uv lock from an index it does not serve
transitive_managed_by_propertyMaven: the managed version comes from a property
transitive_already_managedMaven: already managed at the fixed version or higher
transitive_lock_version_unsupportednpm: lockfileVersion 1. pnpm: a lockfile version other than 9.0
transitive_manifest_not_supportedA lockfile not listed above, such as bun.lock, pdm.lock or Pipfile.lock
transitive_target_metadata_missingThe dependency proxy gave no metadata for the fixed version, for example because a package policy refuses it
transitive_manifest_missing, transitive_lock_missingThe manifest or lockfile is not at the expected path on the default branch
transitive_release_age_pendingThe fixed version is younger than your Release age; ask again after the date the detail names
transitive_release_age_unknownThe dependency proxy gave no publish time for the fixed version
transitive_branch_existsThe branch security-fix/<package>-<version> already exists
transitive_repo_not_writableThe repository is archived or a mirror

For a request through Open fix PR (every ecosystem except Maven), the request keeps No PR: nothing to change and the detail names the reason.

API

npm, pnpm, Yarn, Python and Gradle need nothing beyond POST /orgs/{org}/dependency-updates/security-fixes/requests. For 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>"}'

It answers 201 with the request and its outcome, pr_opened with pr_number and pr_url, or rejected with reason and detail. A refusal before a request exists is a problem of type security-fix-<reason>, for example security-fix-not-transitive. The scope is dependency-updates:write. The request is then listed by GET …/security-fixes/requests like any other.