NestJS Case Study
Verified baseline scan — CVE Lite CLI v1.25.0 · 2026-06-24 (remediation remeasurement on revision
cee51af)
Summary
- Project: NestJS — production-grade Node.js framework used across thousands of enterprise applications
- Revision:
cee51af9118b68511e77e059f0578a3f0a3bcf0d - Lockfile:
package-lock.json(npm monorepo root lockfile at pinned revision) - Baseline findings: 51 unique vulnerable packages (4 critical · 18 high · 25 medium · 4 low)
- Direct vs transitive: 8 direct / 43 transitive
- Time to first actionable fix command: under 30 seconds
- Validated fix commands generated: 9 command groups at baseline (specific versioned targets, not generic
npm audit fix) - After two measured remediation passes: reduced from 51 → 50 → 47 findings
- Usage-aware triage (
--only-used): 51 → 10 findings on the same revision (41 toolchain/transitive packages not statically imported)
What this case study demonstrates
NestJS represents the harder class of dependency remediation: a mature monorepo where most findings are transitive, OSV advisory coverage has expanded since earlier scans, and there is no single batch upgrade that clears the list.
CVE Lite CLI's direct/transitive split makes this immediately visible. In this remeasurement, 43 of 51 findings are transitive — meaning npm audit fix would have little effect and npm audit fix --force would be risky. CVE Lite still surfaces confident first-pass work ([email protected] as a direct fix, then [email protected] as a parent-chain move) separately from the deeper structural issues, so a developer knows exactly where to start.
The tool also names the parent chain for transitive findings. form-data (critical) is reached through deprecated request paths. Multiple ws versions appear across framework and dev tooling. That context is absent from npm audit's flat output and is exactly what a developer needs when deciding which parent upgrade is worth attempting first.
Comparison Note: CVE Lite CLI vs npm audit
Both tools were run against the same package-lock.json on the same machine on 2026-06-24.
| Metric | npm audit | CVE Lite CLI v1.25.0 |
|---|---|---|
| Total reported findings | 56 | 51 |
| Critical | 6 | 4 |
| High | 22 | 18 |
| Moderate / Medium | 25 | 25 |
| Low | 3 | 4 |
| Direct vs transitive breakdown | ✗ | ✓ (8 / 43) |
| Validated fix targets | ✗ | ✓ |
| Breaking change awareness | ✗ | ✓ |
| Parent chain identified for transitive issues | ✗ | ✓ |
| Specific copy-and-run commands | ✗ | ✓ |
Why CVE Lite reports fewer findings — and why that is not a coverage gap:
npm audit counts advisories and dependency paths, not unique packages. A single vulnerable package with multiple advisories, or one that appears in several dependency paths, contributes multiple entries to the total. CVE Lite counts each unique vulnerable package once. That is why npm audit reports 56 here and CVE Lite reports 51.
This deduplication is intentional. npm audit's 6-critical output includes overlapping entries for the same underlying packages under different advisory IDs. CVE Lite surfaces 4 critical unique packages in the lockfile. A developer acting on npm audit's raw count would discover partway through that several entries point to the same fix or to paths with no confident automated command.
CVE Lite does not suppress advisories. Every advisory ID that contributed to a finding is recorded in the IDs column of the full table output (--verbose --all). The deduplication is in the presentation layer, not in the detection layer.
npm audit's fix guidance for NestJS:
To address issues that do not require attention, run:
npm audit fix
To address all issues possible (including breaking changes), run:
npm audit fix --force
CVE Lite generates targeted commands at baseline, including:
npm install [email protected]
npm install [email protected]
On a project where 43 of 51 findings are transitive, npm audit fix is nearly useless. npm audit fix --force would attempt to resolve all breakages simultaneously, with no guidance on which upgrades are safe and which introduce API incompatibilities. CVE Lite orders the output — fix the direct issue first, then the parent-chain upgrade, then reason about the remainder.
Usage-aware triage
NestJS is a full monorepo with source available — most lockfile findings live in test runners, build utilities, and legacy toolchain paths that --only-used can filter out.
Measured on revision cee51af with CVE Lite v1.25.0 · 2026-06-24:
| Scan mode | Findings | Critical | High | Medium | Low | Direct | Transitive |
|---|---|---|---|---|---|---|---|
Lockfile baseline (--verbose --all) | 51 | 4 | 18 | 25 | 4 | 8 | 43 |
--only-used (actionable subset) | 10 | 1 | 7 | 2 | 0 | 7 | 3 |
51 → 10 — an 80% reduction in finding count. The --usage pass marked 41 of 51 findings as not statically imported (gulp toolchain, deprecated request/form-data chains, test-only paths). The --only-used subset surfaces runtime-facing packages like fastify, @grpc/grpc-js, @fastify/middie, and multiple ws paths still referenced from framework code.
Honest limits: --usage uses static import analysis only. It can miss packages loaded dynamically, through build scripts, or via string-based requires. Treat --only-used as a triage accelerator — not proof that filtered findings are unreachable at runtime. See CLI reference.
Before vs After
The first two rows show the lockfile baseline and its --only-used subset. The rows below document two measured remediation passes applied locally on the pinned revision (CVE Lite v1.25.0 · 2026-06-24):
| Stage | Findings | Critical | High | Medium | Low | Direct | Transitive | Command groups |
|---|---|---|---|---|---|---|---|---|
| Lockfile baseline | 51 | 4 | 18 | 25 | 4 | 8 | 43 | 9 |
--only-used filter (same revision) | 10 | 1 | 7 | 2 | 0 | 7 | 3 | 3 |
After first pass ([email protected]) | 50 | 4 | 17 | 25 | 4 | 7 | 43 | 9 |
After second pass ([email protected]) | 47 | 4 | 15 | 25 | 3 | 7 | 40 | 9 |
The finding count dropped from 51 to 47 across two passes. The first pass cleared the one direct fastify finding. The second pass cleared three transitive high/low findings tied to the test toolchain (glob, serialize-javascript, and one diff path). Critical findings did not move — they require deeper parent-chain or replacement decisions, not a single copy-and-run install.
Fix Journey
Upgrading once is rarely enough in a mature JavaScript monorepo.
Pass 1 — direct fix ([email protected])
At baseline, CVE Lite flagged fastify as a direct high-severity finding with a validated non-vulnerable target. That is the correct first move: it is in packages NestJS controls directly and does not depend on guessing a parent chain.
npm install --ignore-scripts --legacy-peer-deps [email protected]
Peer dependency friction is common in large workspaces. The install succeeded with --legacy-peer-deps. The scan dropped from 51 → 50 findings. The only change was clearing the direct fastify entry. Every critical finding and the bulk of transitive toolchain noise remained — as expected.
Pass 2 — parent-chain upgrade ([email protected])
With the direct fix applied, the next productive move was a parent upgrade on the test runner chain. CVE Lite surfaced [email protected] to address transitive diff paths pulled in through the existing Mocha dependency tree.
The first install attempt:
npm install --ignore-scripts [email protected]
failed because of peer dependency conflicts in the NestJS workspace. Retrying with legacy peer resolution:
npm install --ignore-scripts --legacy-peer-deps [email protected]
succeeded, and the scan dropped from 50 → 47 findings.
What cleared in pass 2:
glob(high, transitive)serialize-javascript(high, transitive)- one
diff(low, transitive) path
What remained after pass 2:
diff(high) and anotherdiff(low) path still present through other parents- all four critical findings (
@fastify/middie,form-data,protobufjs,shell-quote) - fifteen high-severity findings including direct
@grpc/grpc-js,multer, andws
This is a common pattern in large JavaScript projects: the right upgrade is identifiable, but executing it runs into install-policy friction before the graph changes. Knowing what to upgrade is only half the problem. The other half is knowing that the install friction is incidental — not a signal that the upgrade was wrong.
After two passes, the scanner still generated nine command groups. That is meaningful: the repository remains in the deeper transitive-and-toolchain bucket where remaining work belongs to parent-chain decisions and replacement-level calls (request, legacy gulp tooling), not a zero-CVE framing.
Why this matters
NestJS is not a neglected project. It has active maintainers, frequent releases, and a large ecosystem. Yet a lockfile scan still surfaced 51 vulnerable packages, 43 of them transitive.
That is the real-world state of dependency graphs in large JavaScript frameworks: most of the risk is not in packages the project controls directly. It lives in the toolchain, in test runners, in build utilities, and in packages that have not had a breaking-change-free upgrade path for years.
For a developer running a pre-release check, the operationally relevant question is not "how many advisories are there?" It is "what do I do right now, and what do I park?" CVE Lite answers that question in under 30 seconds: one direct upgrade, one parent-chain upgrade worth attempting, and an explicit separation of the structural remainder from the confident first-pass work.
That distinction matters especially in CI. A flat advisory count of 56 triggers pipeline gates and developer anxiety without telling anyone what to do. An ordered output with validated direct and parent-chain fixes, plus a clear explanation of the transitive remainder, gives a team enough to act on before shipping.
Scan command
Run from a local NestJS clone checked out at the pinned revision (full source required for --usage / --only-used):
# Full lockfile graph
npx cve-lite-cli . --verbose --all
# Triage: annotate import status per finding
npx cve-lite-cli . --verbose --all --usage
# Actionable subset: statically imported packages only
npx cve-lite-cli . --verbose --all --only-used
Every number in the Before vs After and usage-aware tables comes from live scans of revision cee51af9118b68511e77e059f0578a3f0a3bcf0d on 2026-06-24.
| Field | Value |
|---|---|
| Remediation measurement date | 2026-06-24 |
| Usage-aware measurement date | 2026-06-24 |
| CLI version | v1.25.0 |
| Revision | cee51af9118b68511e77e059f0578a3f0a3bcf0d |
| Lockfile findings (baseline) | 51 |
--only-used findings (baseline) | 10 |
After pass 1 ([email protected]) | 50 |
After pass 2 ([email protected]) | 47 |
Reproduce from a local clone at the pinned revision:
git clone https://github.com/nestjs/nest.git
cd nest
git checkout cee51af9118b68511e77e059f0578a3f0a3bcf0d
# build CVE Lite from the cve-lite-cli repo, then:
node /path/to/cve-lite-cli/dist/index.js . --verbose --all
node /path/to/cve-lite-cli/dist/index.js . --verbose --all --usage
node /path/to/cve-lite-cli/dist/index.js . --verbose --all --only-used
The remediation walkthrough was performed locally against that revision. Dependency changes were applied during the exercise, but they were not committed in the NestJS repository.
Pass 1 and pass 2 installs used --ignore-scripts --legacy-peer-deps where the default npm resolver blocked peer-conflicting upgrades in the monorepo workspace.
Remaining risk after two passes
The post-pass lockfile still contained 47 findings:
- 4 critical
- 15 high
- 25 medium
- 3 low
Critical and high work that did not clear in two passes:
@fastify/middie(critical, direct)form-data(critical, transitive via deprecatedrequestchains)protobufjsandshell-quote(critical, transitive toolchain paths)- direct
@grpc/grpc-js,multer, andwsstill flagged at pass 2 diff(high) still present through non-Mocha parents after onedifflow path cleared
The medium and low remainder is dominated by legacy gulp/braces/micromatch chains, multiple brace-expansion and js-yaml paths, deprecated request, and toolchain-only packages that --only-used filters out for runtime triage but that still exist in the lockfile graph.
This is a useful stopping point for the public study. The scanner surfaced two meaningful first-pass moves, both worked once peer-resolution friction was handled, and the remaining work is clearly in the deeper transitive-and-toolchain bucket — not a zero-findings endpoint.
Baseline findings
Representative critical and high findings at baseline (full 51-package table from --verbose --all):
| Package | Severity | Relationship | Notes |
|---|---|---|---|
| @fastify/middie | critical | direct | Framework middleware surface |
| form-data | critical | transitive | Deprecated request chain |
| protobufjs | critical | transitive | Toolchain / codegen path |
| shell-quote | critical | transitive | Build tooling |
| fastify | high | direct | Cleared in pass 1 (5.8.5) |
| @grpc/grpc-js | high | direct | gRPC integration |
| multer | high | direct | Upload middleware |
| ws | high | direct / transitive | Multiple versions in graph |
| glob | high | transitive | Cleared in pass 2 via Mocha chain |
| serialize-javascript | high | transitive | Cleared in pass 2 via Mocha chain |
| diff | high / low | transitive | Partially cleared in pass 2 |
Run npx cve-lite-cli . --verbose --all against the pinned revision for the complete deduplicated package list with fix hints and advisory IDs.
Want your project reviewed?
If you maintain an interesting JavaScript or TypeScript project and want CVE Lite CLI considered for a public case study, open an issue in the CVE Lite CLI repository.
Please include:
- the repository link
- why the project would make a useful case study
- whether the dependency graph is publicly reproducible
Not every project will be selected. Preference will go to projects that are publicly useful, technically interesting, and strong examples of realistic dependency remediation workflows.