TL;DR
Finding npm package vulnerabilities is easy. npm audit does it in four seconds and hands you 400 findings. The hard part is the next two questions: which of them can actually be reached in your application, and which upgrade fixes the problem without breaking the build.
- Most of the list is not your problem. Applying reachability and runtime context to "critical" findings leaves a small fraction still critical. Call-graph analysis is what tells you which ones.
npm audit fix --forceis not a fix strategy. It resolves advisories by jumping major versions, which is how a security task becomes an outage.- Transitive dependencies are where the volume lives. You did not choose them, you often cannot upgrade them directly, and they dominate the finding count.
- Fix before the merge, not after the release. A gate in CI and an automated pull request cost minutes. A production patch costs a weekend.
Why the Finding Count Is Not the Problem
Every Node project past its first year produces the same experience. You run a scan, you get hundreds of npm package vulnerabilities, and the list is so large that the rational response is to close the terminal.
That reaction is correct, which is the uncomfortable part. Nearly three-quarters of codebases carry high-risk open-source components, and most of what a scanner reports on any given day is genuinely in your dependency tree. The list is accurate. It is just not a work queue.
The gap between an accurate list and an actionable one is context, and it comes down to a handful of questions.
- Is the vulnerable code actually reachable from my application?
- Is anyone exploiting this in the wild?
- Does the thing it affects matter to the business?
A scanner that cannot answer those hands you an inventory and calls it a report.
How to Check npm Packages for Vulnerabilities
There are four practical ways to check npm packages for vulnerabilities, and they answer different questions.
| Method | What it gives you | Where it stops |
|---|---|---|
npm audit | Instant, built in, no setup. Advisories across the full dependency tree | No reachability, no exploit context. Severity only, and --force will break things |
| Dependabot alerts | Automatic alerts on your repository, with upgrade pull requests | Advisory-driven. It does not know whether your code calls the vulnerable function |
| OSV or the GitHub Advisory Database | Authoritative, free, queryable by package and version. Good for one-off checks | A database, not a workflow. You still triage and fix by hand |
| SCA with reachability | The same advisories, filtered by whether the vulnerable code is callable, plus exploit likelihood and a safe upgrade path | Requires a tool in the pipeline |
Start with npm audit because it costs nothing and takes seconds. Do not stop there, because the answer it gives is “here is everything”, and everything is not a plan.
One thing worth knowing if you rely on advisory feeds: the GitHub Advisory Database carries malware advisories for the npm ecosystem, but Dependabot deliberately does not raise alerts for them, because a downstream user usually cannot resolve them by upgrading. That is a structural gap, not a setting you can switch on. Malicious packages need a different control: detection at publication rather than after disclosure. Xygeni’s Malware Early Warning analyses newly published packages across npm, PyPI, Maven and other registries at the moment they appear, using behavioural and anomaly analysis rather than waiting for a signature, and it is included in the free Developer plan. We cover it in malicious npm packages.
The Filters That Turn 400 Findings Into a Short List
This is the part that changes how a team works.
| Filter | The question it answers | What it typically removes |
|---|---|---|
| Reachability | Can execution in my application actually reach the vulnerable function? | The largest cut. Most advisories sit in code the application never calls |
| Exploit availability | Does a working public exploit exist, and is this being exploited in the wild right now? | Separates theoretical risk from weaponised risk. A finding with a public exploit jumps the queue whatever its severity score says |
| Exploit likelihood (EPSS) | How likely is exploitation in the wild in the next 30 days? | High-severity findings nobody is exploiting, which are rarely this week's work |
| Business context | Does the affected service matter, and is it exposed? | Reachable, exploitable findings in systems that carry no meaningful risk |
| Fix availability | Is there a version that resolves this without breaking me? | Separates what you can close today from what needs a workaround |
- Reachability is the first and largest cut. A vulnerability in a package you depend on is only exploitable if execution can actually reach the vulnerable function. Call graph tracing answers that at the function level rather than guessing from the manifest, and it distinguishes components you genuinely use from those that are merely present. Xygeni’s reachability analysis cuts false positives by up to 70%.
Exploit availability is the second, and it is a fact rather than a forecast. A working public exploit, or confirmed exploitation in the wild, changes the priority of a finding regardless of what its severity score says. That distinction has stopped being good practice and become an obligation: under the Cyber Resilience Act, a manufacturer that becomes aware of an actively exploited vulnerability in its product is on a 24-hour reporting clock. A programme that cannot separate “actively exploited” from “high CVSS” cannot meet that clock.
Exploit likelihood is the third, and it is the same question with the prediction put back in. EPSS scores the probability that a vulnerability will be exploited in the wild in the next 30 days, which is a very different question from how bad it would be if it were. A high CVSS score with a negligible EPSS score is rarely this week’s work.
- Business context is the third, and it is the one no generic feed can supply. A reachable, exploitable vulnerability in an internet-facing payment service is not the same finding as the same CVE in an internal reporting tool.
Applied together, those filters routinely turn a list nobody reads into a list somebody finishes. Runtime and reachability context typically leaves only a small minority of “critical” findings still critical. Xygeni treats reachability, exploit availability, EPSS and business context as configurable stages in a prioritisation funnel, up to eight of them, so “reachable and actively exploited” becomes a standing queue rather than a query somebody runs by hand.
Fixing npm Package Vulnerabilities Without Breaking the Build
Finding is the easy half. The reason npm package vulnerabilities sit unresolved for months is that fixing them carries its own risk, and developers know it.
| Fix option | When it is right | What to check first |
|---|---|---|
npm audit fix | Advisory resolves within a semver-compatible range | Usually safe. Re-run tests, then merge |
npm audit fix --force | Almost never, unguarded | It crosses major versions. Treat every result as a breaking change until proven otherwise |
| Targeted direct upgrade | You own the dependency and a patched version exists | Which vulnerabilities disappear, which new ones arrive, and whether the jump breaks your code |
| Transitive resolution | The vulnerable package is four levels down and not yours | The shortest upgrade path in the tree that resolves it, or an override if none exists |
| No fix available | The maintainer has not patched it | Whether it is reachable at all. If it is not, document and move on rather than forcing an upgrade |
| Remove the dependency | The package is barely used or abandoned | Whether anything still calls it. Unused components are the cheapest findings to close |
The question that matters before any upgrade is not “does this patch the CVE”. It is three questions at once: which vulnerabilities disappear with this version, which new ones arrive with it, and does the version jump break my code.
Xygeni shows all three for every vulnerable dependency, so the choice is between visible options rather than a leap. Then Autofix generates the pull request with the patched version, bulk remediation applies multiple fixes in one action, and the Xygeni Bot runs on demand, on pull requests or daily, so the backlog shrinks without anyone scheduling it.
For transitive dependencies, where you cannot simply bump a version you did not choose, the useful output is the shortest upgrade path in the tree that resolves the advisory, not an alert telling you a package four levels down is vulnerable.
Before They Ship: Where the Check Belongs
“Before they ship” is a scheduling claim, and it comes down to three placements.
- In the IDE, so a developer sees the problem while choosing the dependency, which is the cheapest moment to change it.
- On the pull request, where a bot comments with what changed and opens the fix, and where a security gate can fail a build on findings above a threshold. Gating on severity alone is what makes teams disable the gate. Gating on reachable and exploitable findings is what makes them keep it.
- Continuously after release, because a dependency that was clean at merge becomes vulnerable the day an advisory is published. Continuous monitoring across registries is what catches that, without somebody having to remember to rescan.
And the output of all three belongs in one prioritised queue alongside your SAST, secrets, and container findings, including the ones ingested from tools you already run. A vulnerability list that lives in its own console is a list that competes for attention with the work it was supposed to inform.
Ship the Fix, Not the Finding
A scanner that hands you 400 npm package vulnerabilities has not done the work. It has moved the work.
Xygeni narrows dependency findings by reachability, exploit likelihood, and business context, shows what each upgrade fixes and what it might break, and opens the pull request with the patched version. It runs in your pipeline, produces SBOM and VDR output in SPDX and CycloneDX, the evidence CRA, NIS2 and DORA ask for, and puts dependency findings in the same prioritised queue as the rest of your risk, including findings from scanners you are not replacing.
Start free and scan a repository, or see how reachability changes the list.
FAQ
How do I check npm packages for vulnerabilities?
Run npm audit for an instant view, then use a tool with reachability analysis to narrow the list to findings where the vulnerable code can actually be called. Advisory databases like OSV and the GitHub Advisory Database are useful for checking a specific package and version.
Is npm audit fix safe to run?
The non-forced version is usually safe, since it stays within semver-compatible ranges. npm audit fix --force is not: it upgrades across major versions to resolve advisories, and that is where breaking changes come from.
Why do I have so many npm package vulnerabilities?
Because most of them are transitive. You install a handful of direct dependencies and inherit hundreds of indirect ones, each with its own advisory history. Volume is normal. It is the triage that is missing.
What is reachability analysis?
Determining whether execution in your application can actually reach the vulnerable function in a dependency. It is the single biggest noise reduction available in dependency security, because most reported vulnerabilities sit in code your application never calls.
Should I use CVSS or EPSS to prioritise?
Both, for different things. CVSS describes how severe a vulnerability would be if exploited. EPSS estimates how likely exploitation is in the next 30 days. Severity without likelihood produces a queue sorted by the wrong axis.
How often should I check npm packages for vulnerabilities?
Continuously, not on a schedule. A clean scan on Monday means nothing on Wednesday if an advisory lands on a package you already ship.







