Your Dependency Audit Found Ninety Four Issues and Zero Problems
The audit reported ninety four vulnerable dependencies and the team fixed none, rationally, because none were reachable.
The security tooling reported ninety four vulnerable dependencies, several rated critical, and the team, facing a quarter of work and no signal about which mattered, fixed none. This was not negligence. It was the rational response to a metric with no variance in actionability, because every item looked the same, a CVE number and a severity, and none of them said the one thing that decides whether it is a problem: is the vulnerable code reachable from your application.
I keep a private list of these, because the incidents that hurt are always the ones somebody assumed were harmless.
A dependency audit that reports severity without reachability is a fire alarm that rings for every kitchen in the building, and the team that hears it daily stops hearing it, which is exactly the conditioning an attacker hopes for, because the real alert will arrive in the same ignored channel.
This was a Node 22.14 and a Python 3.13 service, audited by the standard tools, and the partition below is the difference between a backlog and a rumour.
The two questions a vulnerability report must answer
A vulnerable dependency is a fact about code you ship. Whether it is a risk is a fact about code you execute, and the two are different questions that the standard report conflates.
Reachability asks whether your application's call graph can reach the vulnerable function, through the dependency's own code paths, with input an attacker can influence. Most vulnerable code in a typical dependency tree is unreachable, because dependencies bundle parsers, platforms and features you never invoke, and the transitive tree is full of code that exists only to support other code you also do not use.
Exploitability, given reachability, asks whether the vulnerable path can be driven by attacker controlled input in your deployment, which depends on your boundaries, your validation and your network position, none of which the CVE's severity score knows.
Severity is the third and least local question, a global estimate of harm if exploited, and it is the only one the report leads with, which is why it misleads.
Why the noise is self reinforcing
The noise trains the behaviour that creates more noise, in a loop. Teams that cannot triage ninety four items defer all of them, the deferred list grows, the tool's count grows, the count becomes a compliance number rather than an engineering one, and the audit degrades into a ritual reported to people who cannot act on it. Meanwhile the genuinely reachable vulnerability, item ninety five, arrives in the same queue with the same formatting, and the queue's credibility, already spent, cannot convey its urgency.
This is the alert fatigue pattern from nobody reads your dashboards, applied to security tooling: a surface without a decision rule per item spends the attention it needs for the one item that matters.
The partition that works
The fix is to add the missing dimension, reachability, and to let it reorder the queue, which modern tooling increasingly does, and which can be approximated by hand where it does not.
First, the reachable and exploited, dependencies whose vulnerable function is in your call graph and for which exploitation is demonstrated or trivial. These are incidents, fixed now, because they are the intersection of both questions, and they are usually a small number, often zero to three, which is the point: the actionable set is small and the noise set is large, and the tool should say so.
Second, the reachable but not currently exploitable in your deployment, fixed on a schedule, with the schedule set by the reachability depth and your boundary exposure, because these are real risks that lack only a current exploit path, and paths arrive.
Third, the unreachable, recorded and deferred without guilt, reviewed when the dependency's usage changes, because an upgrade that starts using a new feature of a dependency can move an item from third to first, which is why the deferral is a watched state, not a deletion.
The hand approximation of reachability is a grep plus reasoning: does your code import the vulnerable package, does the package's own entry points call the vulnerable module, and does your data flow reach it. Imperfect, but it partitions the ninety four into a handful and a pile, which is the whole value.
The upgrade discipline
The audit's other failure mode is the blanket upgrade, where the team, pressured by the count, upgrades everything, which changes hundreds of dependencies at once, introduces the behavioural regressions that mass upgrades bring, and produces an incident that teaches the team that fixing security causes outages, which further entrenches the deferral.
The discipline is the reverse: upgrade the reachable few surgically, with the change reviewed and tested like any change, and let the unreachable many wait for the normal dependency update cycle, where they are batched, tested and shipped with the same care as features. Security upgrades are changes, and changes deserve the same rigour as the rest, per reviewing database migrations in spirit, because the blast radius is identical.
The metric that means something
Replace the count with the partition's summary: the number of reachable vulnerable dependencies, by tier, and the age of the oldest reachable one. That number has variance, has an owner, and has a decision rule, zero reachable is the target, and it is achievable, unlike zero reported, which was never the goal.
The count of ninety four is a property of the ecosystem. The count of reachable is a property of your system, and only the second is yours to drive to zero.
The takeaway
A vulnerability's severity is global and its risk is local, and the local half is reachability and exploitability in your deployment, which the standard report omits. Partition the audit into reachable and unreachable, treat the reachable few as the queue and the unreachable many as a watched deferral, upgrade surgically, and report the age of the oldest reachable vulnerability instead of the count of alarms.
The ninety four is noise by construction, because most shipped code is never executed. The one that is reachable and ignored is the incident, and the audit that cannot tell them apart is the reason it stays ignored. The same "the metric lacks the dimension that decides action" lesson is coverage's, in code coverage is a marketing metric, which is why the two fixes look alike.