Most open source vulnerabilities don't live in the packages a developer actually typed into package.json or requirements.txt. They live two, four, or six layers deeper, in packages pulled in automatically because a direct dependency needed them. A typical npm project with 100 direct dependencies can easily resolve to 700-1,000 transitive ones, and nobody on the team chose most of them by name. When a CVE lands in one of those buried packages, the fix isn't as simple as bumping a version number in a file you control — the vulnerable package is often nested inside someone else's dependency tree, with its own version constraints. Snyk built a large part of its Software Composition Analysis (SCA) product specifically around this problem. This post walks through, at a mechanical level, how Snyk's publicly documented approach identifies transitive vulnerabilities and computes a path to fix them — including what happens when no clean fix exists.
How does Snyk even find a vulnerability that's four layers deep in the dependency tree?
It parses the full dependency graph, not just the manifest file. When Snyk scans a project, it doesn't stop at reading package.json or pom.xml — it resolves the complete tree the same way the package manager would, using the lockfile (package-lock.json, yarn.lock, poetry.lock, Gemfile.lock, etc.) when one is present. That resolved graph captures every transitive dependency at the exact version your build actually installs, because the same library name can resolve to different versions in different branches of the tree (npm and yarn both allow this via nested node_modules). Snyk then matches each resolved package@version against its vulnerability database. This is the same basic principle behind npm audit, but Snyk's database is curated separately from the GitHub Advisory Database and npm's own advisories, with its own research team adding coverage and metadata (CVSS scores, exploit maturity, disclosure dates) that the registry-native tools don't always include.
How does Snyk decide which direct dependency to upgrade when the vulnerable package isn't one you installed directly?
It walks back up the graph to find the shallowest ancestor whose own upgrade would pull in a patched version. Say your project directly depends on package-a@1.0.0, which depends on package-b@2.0.0, which depends on the vulnerable lodash@4.17.15 (vulnerable to the prototype pollution issue fixed in 4.17.19, CVE-2020-8203). You never listed lodash anywhere. Snyk's remediation logic checks whether a newer release of package-a or package-b exists that resolves to a lodash version at or above 4.17.19, using each ecosystem's own semver resolution rules (npm's nested resolution differs from Maven's nearest-wins strategy, which differs again from Python's flat resolver in pip). If package-a@1.2.0 exists and pulls in a fixed package-b that in turn pulls in patched lodash, Snyk surfaces that single version bump as the "recommended upgrade" — even though package-a was never itself vulnerable. This is the core value proposition of transitive remediation: translating a vulnerability three hops away into a one-line change you can actually make.
What does Snyk do when no upstream package has released a fix yet?
It falls back to direct version pinning at the lockfile level, bypassing the dependency chain entirely. Package managers have added native support for this over the past several years — npm and yarn both support overrides/resolutions fields that force a specific transitive package to resolve to a chosen version regardless of what its parent declares, and Snyk generates these entries automatically as part of its fix recommendations for supported ecosystems. In practice this looked exactly like the 2020 minimist prototype pollution case (CVE-2020-7598): the fix (1.2.3) existed, but minimist was buried under packages like mkdirp and optimist that hadn't all released updated releases pointing to it. Forcing an override so every branch of the tree resolved to minimist@1.2.6 or later closed the gap without waiting on a chain of maintainers to publish new versions. Snyk's CLI (snyk fix) and its Fix PRs feature can write these override blocks directly into a project's manifest and open a pull request with the change pre-applied, rather than just describing the fix in a dashboard.
How does Snyk avoid recommending upgrades that will break the build?
It cross-checks the proposed version against the declared semver range and, where supported, runs the resolution forward rather than assuming any newer version is safe to adopt. A recommended upgrade that jumps a major version (say lodash 3.x to 4.x, or a Java library across a breaking API change) carries real compatibility risk, so Snyk's remediation advice distinguishes between an in-range patch bump that a ^ or ~ constraint would already permit, and an upgrade that requires manually widening the version range in the manifest. Snyk surfaces this distinction directly in its "upgrade path" output so a developer can see whether a fix is a same-day merge or something that needs regression testing first. This is also why Snyk's Fix PRs are scoped per-vulnerability-group rather than bundled as one giant dependency bump — smaller, reviewable diffs are more likely to actually get merged instead of sitting stale.
How does Snyk decide which of the hundreds of transitive vulnerabilities to fix first?
It scores each finding using a combination of CVSS severity, exploit maturity, and — for supported languages — reachability analysis, rather than treating every match in the tree as equally urgent. Reachability analysis checks whether the vulnerable function inside the flagged package is actually called anywhere in your application's code paths (directly or transitively), which matters enormously for transitive findings: a critical CVE in a logging library's obscure formatting function is a very different risk if your code never calls that function versus if it's on the hot path. Snyk has extended function-level reachability to a growing set of ecosystems (Java and JavaScript were among the first), and uses it to downrank or flag as "not currently reachable" vulnerabilities that technically appear in the resolved tree but aren't exercised at runtime. Combined with a documented exploit maturity rating (whether a public proof-of-concept or active exploitation exists), this is how Snyk turns a raw list of, say, 40 transitive CVEs into a short list of the handful worth fixing this sprint.
How Safeguard Helps
Transitive dependency risk doesn't stop at knowing which package is vulnerable — it requires continuous, tenant-aware visibility into what's actually deployed, not just what's declared in a manifest at scan time. Safeguard builds and maintains a live dependency graph across your services, correlating resolved package versions from lockfiles and build artifacts with the vulnerability intelligence needed to tell you which transitive findings sit on a reachable code path versus which are dead weight in the tree. For teams managing SOC 2 and broader compliance obligations, that mapping also feeds directly into audit evidence: you get a record of what was vulnerable, when it was identified, and how remediation (upgrade, override, or accepted-risk exception) was applied, tied to the actual service and environment rather than a point-in-time scan. If your team is trying to close the gap between "we have a long tail of transitive CVEs" and "we know exactly which ones matter and can prove we handled them," that's the layer Safeguard is built to sit on top of.