A scanner reports a vulnerability in a package you have never heard of, in a project where you declared forty dependencies and installed six hundred. The useful question is not whether it is real. It is how it got there — because that determines the fix.

The gap between what you declared and what you installed is the normal condition of modern software, not a sign that something went wrong. You asked for a web framework; it needed a router, a body parser and a template engine; each of those needed their own. The tree expands quickly and quietly.

Two terms are worth pinning down, because the distinction drives everything else:

  • A direct dependency is one your project declares, in its manifest, on purpose.
  • A transitive dependency is one that arrived because something else needed it. Nobody on your team chose it, and often nobody on your team knows it exists.

In most projects the great majority of vulnerabilities land in the second category — the packages nobody chose consistently outnumber the ones somebody did. That is the whole reason the "why do I have this?" question matters operationally rather than academically.

The chain is the actionable part

A finding that says qs has a vulnerability is close to unusable if you did not install qs. There is no line in your manifest to change.

A finding that says express › body-parser › qs is immediately actionable, because it tells you the fix is almost certainly to upgrade express. The maintainers of express updated body-parser, which updated qs, and one version bump at the top of the chain resolves the whole path.

This is why a good scanner shows the shortest path from something you actually declared down to the vulnerable package. It converts "an inexplicable finding about a stranger's code" into "upgrade this one thing you already know about".

When the upgrade is not available

Sometimes the top of the chain has not released a version that pulls in the fixed dependency. Then you have three options, in descending order of preference:

Wait, if the risk allows it. Check whether the vulnerability is actually reachable in your usage and whether it is being exploited. A vulnerability nobody is exploiting, in a code path you never call, can legitimately wait for the upstream release — provided that judgement is recorded rather than forgotten.

Override the version. Most ecosystems have a mechanism to force a transitive dependency to a specific version — npm has overrides, Yarn has resolutions, Maven has dependency management. This works and is widely used, but it is a local decision to run a combination the upstream maintainers have not tested. Note it, and remove it once the real upgrade lands.

Replace the direct dependency. Rare, disruptive, and occasionally correct — usually when the parent turns out to be unmaintained, which is a bigger problem than the vulnerability that surfaced it.

Why your tools disagree about the count

If two scanners report different numbers for the same project, the explanation is usually not that one is wrong. It is that they read different things.

A lockfilepackage-lock.json, poetry.lock, composer.lock, Cargo.lock — records the exact resolved tree, transitives included. Reading it gives the real answer.

A manifestpom.xml, a plain requirements.txt, package.json alone — records intent. It lists what you asked for, not what you got. A tool that reads only the manifest sees your forty packages and none of the six hundred, and unless it says so, that reads as a much healthier project than you have.

This is the strongest practical argument for committing your lockfile. It is not only about reproducible builds: without one, nobody — including you — can enumerate what your application actually contains.

Development-only dependencies deserve a nuance

Some of the tree is build tooling that never reaches production. A vulnerability there is genuinely less exposed, and treating it with the same urgency as a flaw in a payment library is how a developer learns to ignore the scanner.

But less exposed is not unexposed, and the exception matters: a malicious package in your build tooling runs on developer machines and CI runners, with repository credentials and cloud tokens in the environment. Downgrade the severity of a vulnerable dev dependency if you like. Never downgrade a malicious one.

What is a transitive dependency?

A transitive dependency is a package that arrived in your project because another package needed it, rather than because you declared it. If you install a web framework and it depends on a router, and that router depends on a parser, the router and the parser are transitive. In most projects they vastly outnumber the packages you actually chose, and they are where the majority of vulnerabilities are found.

Why does my scanner report a vulnerability in a package I never installed?

Because the package is a transitive dependency: something you did install depends on it. There is no line in your manifest to change, which is why a bare finding is hard to act on. The useful information is the dependency chain — the shortest path from something you declared down to the vulnerable package — because upgrading the package at the top of that chain usually resolves the whole path in one step.

How do I fix a vulnerability in a transitive dependency?

First try upgrading the direct dependency that pulled it in, since maintainers usually update their own dependencies and one version bump resolves the chain. If no such release exists yet, most ecosystems let you force a specific version of a transitive package — npm overrides, Yarn resolutions, Maven dependency management — which works but means running a combination upstream has not tested, so record it and remove it once the real upgrade lands.

Why do different scanners report different dependency counts?

Different scanners usually report different dependency counts because they read different files. A lockfile records the exact resolved tree including every transitive package, so a tool reading it gives the real number. A manifest such as pom.xml or a plain requirements.txt records only what you asked for, so a tool reading that alone sees your declared dependencies and none of the ones they pulled in. That difference is why committing a lockfile matters beyond reproducible builds.

Where CodeControl fits

CodeControl computes the shortest chain from something your project actually declared to every transitive finding — express › body-parser › qs — and shows it on the finding, the tables and the inventory, along with a note about which upgrade usually resolves it. Where a format has no dependency edges it shows nothing rather than a guess. Development-only dependencies report one severity notch calmer with the reason stated, though malware verdicts ignore the classification entirely.