You create a new Node project. You need an HTTP server, a database driver, and a schema validation library. Three dependencies. You run npm install and walk away.
When you come back, node_modules has 183 folders in it.
This is not a bug. But it creates a security problem most teams are not thinking about clearly: your actual attack surface is not the three packages you chose. It is the 180 you didn't.
Direct vs. transitive
A direct dependency is a package you explicitly listed in your package.json. You made a deliberate choice to include it.
A transitive dependency is a package that one of your dependencies needs — or a package that that package needs, recursively. You never chose it. You may never have heard of it.
A typical React application has around 30 direct dependencies and over 1,000 transitive ones. A medium-sized Python FastAPI service might pull in 80 packages from a requirements.txt with 12 entries. Every node in that tree is code that runs in your process, with your credentials, on your infrastructure.
The probability math follows: if each of those 1,000 packages has even a 0.5% chance of having a known, unpatched CVE, the probability that your project is running at least one vulnerable transitive package approaches 100%. Multiple studies consistently find that roughly 40% of known security vulnerabilities in npm packages are only reachable via transitive paths.
The Log4Shell lesson
In December 2021, CVE-2021-44228 in Apache Log4j became one of the most widely exploited vulnerabilities in years. Log4j was rarely a direct dependency — most applications pulled it in via other frameworks that logged internally. If your pom.xml didn't list log4j-core directly, a naive audit of your direct dependencies gave you a false all-clear.
The teams that were prepared had already built a complete picture of their transitive dependency tree. They searched their SBOM for any version of log4j-core across all projects and got an answer in minutes. Teams without that visibility spent days manually checking projects — or assumed they were fine.
Log4Shell was not an unusual event. It was a preview. The same dynamic applies to any widely-used utility library sitting three or four levels deep in the tree that develops a critical vulnerability.
Why most teams have no visibility past depth 1
Lock files solve reproducibility, not visibility. A package-lock.json records the exact resolved version of every package — essential for reproducible builds. But lock files are tens of thousands of lines long and not designed to be read by humans. The information is there; extracting security-relevant data from it by hand is impractical.
Depth 1 audits give a false sense of coverage. Many teams run npm audit or pip-audit. These tools do check transitive dependencies — but when a report flags a package the developer doesn't recognize, it is frequently dismissed as a false positive or someone else's problem. The "you got this transitively via X via Y via Z" chain is not obvious from the output.
The dependency tree changes without you noticing. You did not change any dependency last week. But did any of your dependencies release a patch that quietly added or upgraded a transitive dependency? Package managers do not alert you when the graph changes beneath a pinned version.
What auditing actually requires
The correct artifact for transitive dependency analysis is a full SBOM — a machine-readable document listing every component at every depth, with version and provenance.
# Generate a CycloneDX SBOM for an npm project
npm sbom --sbom-format cyclonedx --output-format json > sbom.json
# Or with Syft
syft . -o cyclonedx-json > sbom.json
With an SBOM, you can ask questions that are impossible from package.json alone:
- Which packages in my tree depend on
minimatch < 3.1.2? - How did
lodash@4.17.11end up here? Which direct dependency brought it in? - Do any of my 12 microservices share a vulnerable version of this library?
That third question is what Log4Shell made urgent. Without a centralized view of transitive dependencies across services, answering it meant going service by service.
When triaging transitive vulnerabilities, two signals beyond CVSS matter: CISA KEV (is this being actively exploited in the wild?) and EPSS (what is the 30-day exploitation probability?). A 7.5-rated CVE with an EPSS score of 0.02% is a different priority from one with an EPSS score of 40%.
Practical first steps
Run a full audit today. npm audit traverses transitive dependencies. So does pip-audit and govulncheck ./... for Go. If you have not run one recently, the output will likely surprise you.
Pin lock files in CI. If your pipeline runs npm install without a lockfile, it may resolve different versions than your local environment. Use npm ci in automated pipelines — it installs exactly what the lockfile specifies.
Generate one SBOM and read it. Generating a CycloneDX SBOM for one project and opening the JSON file is a useful calibration exercise. Seeing the full component list — often 10 to 30 times more packages than your manifest — resets your sense of actual attack surface.
The three packages you added last sprint are probably fine. The 180 packages that came with them — that is where the audit belongs.
Lumstep runs software composition analysis on every repository scan, generating a full SBOM and enriching findings with CISA KEV and EPSS data so the report tells you what needs fixing now, not just what is technically flagged.
Ou laissez Lumstep s'en charger.
Connectez un repo et Lumstep le scanne automatiquement - secrets, dépendances, SBOM et qualité du code - et ouvre la PR de correction.