Your direct dependencies are the easy part. The harder part is everything they bring with them.
A modern application rarely installs only the packages listed by hand in its manifest. Add one framework, SDK, test runner, or build tool, and the package manager resolves a larger tree behind it. Those indirect packages are transitive dependencies. Your team may never import them directly, but they can still end up in your build, your runtime, or your development toolchain.
That is why transitive dependency vulnerabilities are easy to miss in review. A pull request may show a small change to package.json, pyproject.toml, requirements.txt, or another manifest. The lockfile may tell a bigger story: new packages, changed versions, and a different resolved tree than the one you shipped last week.
For security work, that difference matters. The manifest shows what the project asked for. The lockfile gets closer to what the package manager actually installed.
What a transitive dependency is
A direct dependency is a package your project declares explicitly. A transitive dependency is a package pulled in by one of those direct dependencies, or by another package further down the chain.
A simple chain might look like this:
your app -> web framework -> utility package -> vulnerable versionThe vulnerable package may not appear in the short dependency list a developer checks during review. It may not appear in application imports either. But if it is part of the resolved dependency tree, a scanner can still report it when the package name and version match a known advisory.
That does not automatically mean the application is exploitable. It means the finding deserves triage.
Why the lockfile changes the review
Manifests and lockfiles answer different questions.
- A manifest lists the dependencies a project declares, often with acceptable version ranges.
- A lockfile records the resolved package versions selected for an install.
- A lockfile diff can show indirect package changes that are not obvious from the manifest alone.
In npm, package-lock.json describes the generated dependency tree and supports repeatable installs. Other ecosystems use different files, such as yarn.lock, pnpm-lock.yaml, poetry.lock, Gemfile.lock, and Cargo.lock. The formats are different, but the security reason for looking at them is similar: scanners need concrete package versions, not just top-level intent.
This is also why two projects with similar manifests can produce different vulnerability results. If their resolved versions differ, their risk picture can differ too.
Why transitive vulnerabilities slip through
Transitive risk is not mysterious. It is just awkward.
- The package is hidden from the normal review path. Developers tend to inspect the dependency they chose, not every indirect package it installs.
- The fix may live one level up. You often need to update the parent dependency that brought in the vulnerable package.
- Severity is only one input. CVSS can help with triage, but teams still need to ask whether the affected code path is reachable in their application.
- Development dependencies can still matter. A vulnerable test helper or bundler plugin may not ship to production, but it can still affect builds, local machines, or release tooling.
- Lockfile changes are noisy. One small manifest edit can move dozens of indirect packages. That noise makes security-relevant changes easy to skim past.
The mistake is assuming nobody touched the vulnerable package because nobody edited it directly. In dependency graphs, indirect change is normal.
How to review a lockfile without reading every line
Nobody should review a thousand-line lockfile by hand line by line. The useful habit is simpler: scan the resolved tree, pay attention to new findings, and use the dependency path to decide what to do next.
Scan dependency changes in CI
Run dependency vulnerability scanning on pull requests or CI builds, and include the lockfile when the ecosystem supports it. That catches cases where a safe-looking manifest change introduces a vulnerable transitive package.
If a team does not want to give a third-party tool full repository access, dependency-file scanning can be a lower-access starting point. Depna fits that model: it focuses on scanning dependency files rather than requiring the whole codebase. That is useful for visibility, but it should not be confused with a complete application security review.
Treat lockfile-only changes as security-relevant
A pull request that changes only a lockfile can still change what gets installed. That does not make every lockfile diff suspicious. It does mean the diff should run through the same dependency checks as any other package change.
Ask what changed, not just what failed
When a scanner reports a finding, start with the path. Most package managers can explain why a package is present. In npm, npm ls <package> and npm explain <package> can show which dependency introduced it.
Once you know the parent package, look for the smallest safe fix. Sometimes that means updating the direct dependency and regenerating the lockfile. Sometimes there is no compatible parent update yet. In those cases, teams usually choose between waiting with documented risk, using an override where the ecosystem supports it, replacing the parent package, or applying a temporary mitigation outside the package manager.
Overrides deserve care. They can be useful, but they may force a version the parent package has not tested against. Let the build and test suite have a vote.
A practical triage checklist
| Question | Why it matters | What to check |
|---|---|---|
| Where did the package come from? | The fix often depends on the parent package. | Dependency path from the app to the vulnerable package. |
| Is it runtime, development, or build tooling? | Impact and urgency depend on where the package is used. | Production dependency tree, dev dependency tree, build scripts, and CI jobs. |
| Is a patched version available? | A finding is easier to resolve when the ecosystem already has a safe upgrade path. | Advisory details, fixed version ranges, and parent package releases. |
| Can the parent dependency be upgraded safely? | Transitive fixes often arrive through the direct dependency that introduced the package. | Release notes, breaking changes, test results, and lockfile diff. |
| Is the affected code reachable? | A matched advisory is not the same as proven exploitability in your app. | Usage, configuration, exposed routes, build context, and compensating controls. |
| What is the decision? | Repeated alerts become noise if nobody records the reason for action or deferral. | Owner, reason, fix plan, accepted-risk note, and revisit date. |
Where continuous scanning helps
A lockfile can be unchanged and still become risky later. New advisories are published, existing advisories are corrected, and vulnerability databases get more detail over time.
CI scanning catches risk introduced by code or dependency changes. Scheduled scanning catches newly disclosed risk in dependencies you already have. The two checks solve different problems. Teams that only scan during pull requests can miss advisories published after the code was merged.
What lockfile scanning cannot prove
Lockfile scanning is good at matching package names and versions to known advisories. It is not good at knowing your business logic, deployment model, or runtime reachability.
It also will not replace source review, secret scanning, container scanning, runtime testing, or human judgment. That is fine. The point is not to make the lockfile carry the whole security program. The point is to stop treating it as generated noise when it contains the resolved dependency tree your build depends on.
The habit worth keeping
For every dependency-changing pull request, ask two questions:
- What changed in the resolved dependency tree?
- Did that change introduce a known vulnerability?
That small habit catches issues that a manifest-only review can miss. It also gives engineers a better starting point for triage: not panic, not blind acceptance, just a clear path from advisory to dependency path to fix decision.