Safeguard
Vulnerability Management

Upgrade Impact Analysis: Predicting Breaking Changes Befo...

Why 70% of security patches sit unapplied for months, and how diffing a package upgrade against your call graph predicts breaking changes before you run npm update.

Priya Mehta
DevSecOps Engineer
7 min read

When you run npm update or bump a version pin in your lockfile, you're placing a bet: that the maintainers' semver promises hold, that the changelog is complete, and that nothing downstream breaks. That bet fails more often than teams admit. A 2023 Endor Labs survey found that over 50% of engineers who delayed a dependency upgrade did so specifically because they feared it would break something in production — not because the patch itself was risky, but because nobody could tell them what would happen if they applied it. Endor Labs answered that fear with reachability analysis for vulnerabilities. But reachability tells you whether a known CVE affects you — it says nothing about whether the fix for that CVE will break your build. Upgrade impact analysis closes that gap: it inspects the actual diff between your current version and the target version, maps it against your call graph, and tells you — before you touch the lockfile — which of your files, functions, and CI jobs are about to feel it.

What Is Dependency Upgrade Impact Analysis?

Dependency upgrade impact analysis is the practice of statically diffing two versions of a package and simulating the blast radius of that diff against your own codebase before you install it. Where reachability analysis answers "is this vulnerable code path exercised by my app," upgrade impact analysis answers a different, later-stage question: "if I move from lodash 4.17.20 to 4.17.21, or from express 4.x to 5.x, which of my functions call the changed APIs, and do the changed signatures still match what I'm passing?" Concretely, this means parsing the package's exported symbols at both versions, diffing function signatures, removed exports, and changed default behaviors, then cross-referencing that diff against your own import graph. A tool doing this well can tell you, in seconds, "3 files in src/billing/ call stripe.charges.create(), and the target version renames that method's third argument from metadata to meta" — instead of you finding out in a failed deploy at 4:52 PM on a Friday.

Why Do 70% of Available Security Patches Sit Unapplied?

Because teams can't distinguish a safe patch from a breaking one without doing the upgrade first, and that verification cost is what actually stalls patching, not laziness. Sonatype's 2023 State of the Software Supply Chain report found that the median time to update a vulnerable open source dependency is 76 days, even when a fix is available on day one. Endor Labs' own 2024 research pegged reachable-but-unpatched exposure at similarly long windows, arguing (correctly) that most CVEs never get exercised by the app that carries them — so teams should triage by reachability rather than patch everything. That's sound advice for prioritization, but it doesn't solve the second-order problem: once a vulnerability is reachable and does need patching, the team still has to find out if the upgrade itself is safe. Reachability narrows the queue; it doesn't clear it. In practice, engineers we've talked to describe the same pattern: they know the CVE is real, they know the patch exists, and they still push it to next sprint because the changelog reads like it might touch three services and nobody has bandwidth to regression-test all three speculatively.

How Does a Semver-Minor Bump Still Break Production?

It breaks production when the maintainer's semver discipline doesn't match the actual code diff, which happens on a routine, almost predictable basis in the npm and PyPI ecosystems. The node-ipc incident in March 2022 is the extreme case — a "patch" version silently wiped files on machines with Russian or Belarusian IP addresses — but the mundane version of this happens weekly: a "minor" release deprecates a function with a console warning today and removes it in the next minor six weeks later, technically staying inside semver's letter while breaking anyone who didn't read the deprecation notice. Express 5.0, released as a major after nearly a decade of Express 4.x dominance, removed several implicit type coercions in routing and dropped support for some middleware signatures — changes that are correctly flagged as breaking by semver, but that still catch teams off guard because the actual affected surface (which routes, which middleware) is buried in a migration guide, not surfaced against their specific codebase. Whether the break is a semver violation or a fully disclosed major-version change, the operational question is identical: does my code touch the part that changed? That's a question changelogs and semver headers structurally cannot answer, because they describe the package in the abstract, not your usage of it.

What Does an Impact Report Actually Need to Show?

It needs to show the specific call sites in your code that touch the changed API surface, not a generic risk score for the package as a whole. A useful upgrade impact report answers four things concretely: which of your files import the changed module; which changed functions or exports those files actually call; whether the call's argument shape (arity, types, required vs. optional) still matches the new signature; and whether any transitive dependency in your tree pins an incompatible range that will force a resolution conflict. For example, upgrading axios from 0.x to 1.x changed how AxiosError is constructed and how request cancellation works via AbortController instead of the old CancelToken. A report that just says "axios has breaking changes in 1.0" is trivia. A report that says "src/api/client.ts:44 and src/jobs/sync.ts:112 use axios.CancelToken.source(), which is deprecated and removed behavior in 1.7+" is something an engineer can act on in the same sitting they read it, without pulling the branch and running the full test suite speculatively.

Can You Predict Breakage Without Running the Full Test Suite?

Yes, for a meaningful subset of breaking changes — static analysis catches signature mismatches, removed exports, and altered default parameters before a single test executes, though it can't substitute for runtime behavior changes like altered error timing or new validation strictness. This is a precision-versus-completeness tradeoff worth being explicit about: static diffing of the package's public API against your call graph will reliably catch "you're calling a function that no longer exists" or "you're passing a positional argument the new version expects as a named option." It will not catch "the library now validates email format more strictly and throws where it used to silently pass," because that's a behavioral change invisible to a type or signature diff. The practical answer is to use impact analysis as a fast, pre-merge filter that resolves the majority of upgrades in seconds — Endor Labs' own material cites roughly 95% of vulnerabilities as unreachable in typical codebases, and a similar long tail of upgrades turn out to be genuinely safe once you can see the diff — and reserve full CI test runs for the smaller set of upgrades the static pass flags as touching your actual call sites.

How Safeguard Helps

Safeguard treats upgrade safety as a first-class signal alongside reachability, not an afterthought you discover after the CVE fix is already merged. When a scan surfaces a fixable vulnerability, Safeguard doesn't just show you the target patch version — it diffs the current and target package versions, maps the changed exports and signatures against your actual import graph, and flags the specific files and call sites that will be affected, right next to the CVE it's remediating. That means a patch recommendation comes with two numbers instead of one: how exploitable the vulnerability is in your environment, and how much of your codebase the fix will touch. For teams managing hundreds of repos across a monorepo or microservice fleet, Safeguard rolls this up so you can see, at a glance, which upgrades are one-click safe (no call-site impact, no breaking signature changes) and which need a human to review a specific diff before merging. Combined with reachability-aware prioritization, this turns "76 days median time to patch" into a queue you can actually clear — because the teams doing the patching finally know, before they start, what they're going to break.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.