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.
| Ecosystem | How the package is pinned | How 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 version | Open 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 match | As for npm |
Yarn 1 (yarn.lock) | A resolutions entry in package.json, and the yarn.lock entry moved | As 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.toml | As for npm |
Maven (pom.xml) | A <dependencyManagement> entry in the pom.xml the finding came from | Open fix PR, as for npm |
| Maven (seen in your SBOM) | A <dependencyManagement> entry in the root pom.xml | Pin 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 moved | As for npm |
| Go | Nothing extra: go.mod lists every module your build uses, marked // indirect, and the fix raises that line | Open 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.jsonislockfileVersion2 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 inpackage.jsonpnpm.overridesinstead whenpackageManagernames pnpm 9 or older, or, with nopackageManager, when the project keeps its overrides there and has nopnpm-workspace.yaml; pnpm-lock.yaml: itsoverrides:section, the package’spackagesandsnapshotsentries 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
sdistandwheels, 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, orfiles.pythonhosted.orgwhen you lock against PyPI. A lock from any other index is refused withlock_entry_unsupported. - Poetry: the version, the description,
python-versionsandfiles. Poetry’scontent-hashcoverspyproject.tomlonly, 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 sha256yourself; - the package is locked in a configuration an
implementationconstraint 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:
| Reason | Meaning |
|---|---|
transitive_already_declared | The package is a direct dependency. Open fix PR bumps it where it is declared |
transitive_transitive_dependencies_changed | The fixed version declares other dependencies (for Python, also another Requires-Python), so the lock needs a full resolve |
transitive_override_conflict | Another copy of the package, at an unaffected version, would move too, or an override, resolution or constraint already pins it differently |
transitive_yarn_berry_checksum | Yarn 2 or later: its lock needs a checksum only Yarn can compute |
transitive_gradle_verification_metadata | Gradle: the build verifies dependencies, and the new version needs checksums Gradle computes |
transitive_gradle_configuration_unsupported | Gradle: the package is locked in a configuration an implementation constraint does not reach |
transitive_build_file_unsupported | Gradle: the build file has no top-level dependencies { block |
transitive_lock_entry_unsupported | The 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_property | Maven: the managed version comes from a property |
transitive_already_managed | Maven: already managed at the fixed version or higher |
transitive_lock_version_unsupported | npm: lockfileVersion 1. pnpm: a lockfile version other than 9.0 |
transitive_manifest_not_supported | A lockfile not listed above, such as bun.lock, pdm.lock or Pipfile.lock |
transitive_target_metadata_missing | The dependency proxy gave no metadata for the fixed version, for example because a package policy refuses it |
transitive_manifest_missing, transitive_lock_missing | The manifest or lockfile is not at the expected path on the default branch |
transitive_release_age_pending | The fixed version is younger than your Release age; ask again after the date the detail names |
transitive_release_age_unknown | The dependency proxy gave no publish time for the fixed version |
transitive_branch_exists | The branch security-fix/<package>-<version> already exists |
transitive_repo_not_writable | The 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.