npm Dependency Scanner
Your application is mostly other people's code. This scanner reads package.json from the repository root and every workspace, strips each version down to a bare number, and asks the OSV.dev vulnerability database whether that package at that version has a published advisory. It reads no lock file, and one advisory becomes one finding.
From manifest line to OSV query
The scanner has no vulnerability table of its own. Everything it knows comes back from api.osv.dev, so the work here is turning a manifest into the right questions. The actor is the scanner; the only other party is OSV.
1The scanner starts from package.json in the repository root and from the package.json of every workspace the repository snapshot lists. Both dependencies and devDependencies count.
2Names are merged into one map. The root is read first and wins on a clash, then each workspace adds only names not yet seen; a name in both sections of one file takes the devDependencies value. Only the first 200 entries are queried.
3Each version string is cleaned by stripping leading ^ ~ > = < and spaces. ^13.0.0 becomes 13.0.0. The result is the version written in the manifest, not the one a lock file resolved, because no lock file is read.
4Queries go to /v1/querybatch in batches of 100 with a 30 second timeout. Each query names the package, the npm ecosystem and the cleaned version. An HTTP error loses that batch (a warning is logged) and the scan carries on.
5OSV answers with a results array in query order. Every advisory in a result becomes its own finding at confidence 0.95, so a package with two advisories yields two findings. The finding's file is the manifest the name came from, root or workspace.
How a severity is chosen
An OSV record can describe severity two ways, and the scanner's rule for choosing is short. Whichever way the record goes, an unreadable severity now falls back to Medium. The batch response is first enriched with the full record from /v1/vulns/{id}, and database_specific.severity is read before the score.
1Three record shapes, each handled by a different branch of _extract_severity. The next steps highlight each in turn.
2A bare number is thresholded when the record has no recognised label: 9.0 and above is CRITICAL, 7.0 HIGH, 4.0 MEDIUM, anything lower LOW. A score of 9.8 is Critical.
3A CVSS v3.x vector string is decoded. The shared helper computes the base score from the metrics and applies the same thresholds.
4database_specific.severity is checked first and mapped by name: CRITICAL, HIGH, MODERATE (as Medium), MEDIUM and LOW. MODERATE is how GitHub advisories spell Medium. A missing or unrecognised label becomes MEDIUM.
OSV's schema stores CVSS entries as vector strings; the scanner now computes the CVSS v3.x base score from them, so Low, Medium, High and Critical all follow the real score.
Odd version strings
Cleaning is a plain left-strip of ^ ~ > = < and spaces. It handles the common prefixes and nothing else; whatever remains is sent to OSV exactly as written.
1Four kinds of version string a manifest can hold, and what the strip leaves behind.
2^4.17.15 loses its caret and is queried as 4.17.15. This is the common case and it works as intended.
3~1.2.3 becomes 1.2.3. A range allows newer versions, and the lock file may have installed one, but the scanner asks about the floor only. A newly patched install can still be reported, and a newer vulnerable one is not.
4A compound range such as >=1.0.0 <2 keeps everything after the first version. The query version is 1.0.0 <2; what OSV does with it is OSV's decision.
5latest, * and workspace:* pass through unchanged. If OSV answers a batch with an HTTP error, all up to 100 packages in that batch are dropped with a logged warning and a progress message that N packages could not be checked; the scan reports no findings for them.
Why it matters
A typical Next.js app has 500 to 1000 transitive dependencies, and one vulnerable package can allow remote code execution, bypass authentication (a JWT library bug means forged tokens work), crash the application through crafted input, or leak data through error handling. You do not need a bug in your own code: the bug in a dependency is your bug too.
147 million records were stolen from Equifax through an unpatched Apache Struts dependency (CVE-2017-5638). The patch had been available for two months, and Equifax paid $700 million in settlements. CVE-2021-44228 in Log4j allowed remote code execution through crafted log messages and affected hundreds of thousands of applications, including services from Apple, Amazon, Cloudflare and Steam.
Vulnerable vs fixed code
{
"dependencies": {
"next": "13.0.0", // multiple critical CVEs
"jsonwebtoken": "8.5.1", // algorithm confusion attack
"lodash": "4.17.15", // prototype pollution
"axios": "0.21.0" // ReDoS
}
}# Check for vulnerabilities
npm audit
# Update to patched versions
npm update
# Major version updates (breaking changes)
npx npm-check-updates -u
npm install
# Automate with Dependabot or Renovate in CI
# .github/dependabot.yml
# version: 2
# updates:
# - package-ecosystem: npm
# directory: /
# schedule: { interval: weekly }Limits and what it needs
| Item | Value | Effect |
|---|---|---|
| Files read | package.json at the root and in each workspace | No package-lock.json, yarn.lock or pnpm-lock.yaml; transitive packages are never queried |
| Sections | dependencies and devDependencies | peerDependencies and optionalDependencies are ignored |
| Package cap | 200 after merging | The rest are not checked |
| Batch | 100 queries per request, 30 s timeout | An HTTP error drops that batch and logs a warning |
| Finding | one per advisory, confidence 0.95 | Title Vulnerable dependency: name@version |
| Severity | numeric score or CVSS v3 vector thresholds, label map, default Medium | Range Low to Critical |
| Needs | outbound HTTPS to api.osv.dev, no API key | Offline, the scan returns no findings from this scanner, logs a warning and emits a progress message ("N of M package(s) could not be checked") |
The scanner does not run npm audit; it queries OSV.dev, and the configuration holds an "outdated package" title, a confidence of 0.50 and a lock-file error message that the scanner never uses. The file attached to a finding is found by checking which package name appears in the finding's title, so a short name that is contained in a longer one (react inside react-dom) can be attributed to the wrong manifest. The batch endpoint returns only advisory ids, so the scanner fetches the full record for each unique id (capped at 100) before reading severity, summary and fixed version; if that fetch fails the finding falls back to Medium. A failed batch query is logged and reported as packages that could not be checked. The related Java, Python and OSV scanners cover other ecosystems.