SAST scanner · asks OSV.dev

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.

SASTLow–CriticalDependency Vulnerabilityneeds api.osv.dev200 packages max

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.

package.json (root) dependencies "next": "^13.0.0" "lodash": "~4.17.15" devDependencies "jest": "^29.0.0" packages/web/package.json "lodash": "4.17.21" "axios": ">=0.21.0" workspace manifest One name, one version built in the next step next13.0.0^13.0.0 lodash4.17.15~4.17.15 jest29.0.0^29.0.0 axios0.21.0>=0.21.0 root wins: workspace lodash 4.17.21 is dropped, never queried list is cut at 200 packages prefix ^ ~ > = < stripped api.osv.dev POST /v1/querybatch {"package": {"name":"lodash", "ecosystem":"npm"}, "version":"4.17.15"} one query per package 100 queries per request results[i] answers query i results[1].vulns two advisories for lodash Findings none yet HIGH · 0.95lodash@4.17.15advisory 1 summaryfile: package.json HIGH · 0.95lodash@4.17.15advisory 2 summaryfile: package.json one finding per advisory (illustrative response)

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.

record carries a bare number "severity": [{"score": "9.8"}] float(score), then thresholds <4 LOW≥4 MEDIUM≥7 HIGH≥9 CRITICAL 9.8 is above 9.0 CRITICAL record carries a CVSS vector "score": "CVSS:3.1/AV:N/AC:L/…" CVSS v3.x vector: base score computed, same thresholds OSV defines score as a vector string for CVSS entries used only when no recognised label is present; first usable score wins HIGH record has a recognised label database_specific.severity label, case-insensitive, mapped by name CRITICAL, HIGHMODERATE, MEDIUM → MEDIUMLOW a missing or unknown label falls back to MEDIUM as labelled

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.

What this means in practice

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.

manifest sayssent to OSV as versionwhat that means ^4.17.154.17.15a clean version: the normal case ~1.2.31.2.3the written floor, not the version npm installed >=1.0.0 <21.0.0 <2text after the first version survives the strip latest · * · workspace:*unchangednot versions at all; no resolution is attempted

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

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.

Equifax (2017) and Log4Shell (2021)

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
  }
}

Limits and what it needs

ItemValueEffect
Files readpackage.json at the root and in each workspaceNo package-lock.json, yarn.lock or pnpm-lock.yaml; transitive packages are never queried
Sectionsdependencies and devDependenciespeerDependencies and optionalDependencies are ignored
Package cap200 after mergingThe rest are not checked
Batch100 queries per request, 30 s timeoutAn HTTP error drops that batch and logs a warning
Findingone per advisory, confidence 0.95Title Vulnerable dependency: name@version
Severitynumeric score or CVSS v3 vector thresholds, label map, default MediumRange Low to Critical
Needsoutbound HTTPS to api.osv.dev, no API keyOffline, 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")
Scope note

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.