Fast, no-auth CVE scanner for Node.js projects. Free OSV.dev integration. Works as CLI and CI gate.
# Clone or download the repo, then build once:
cd cve-watch && npm run bundle
# Run locally from any project directory:
cd your-node-project
node /path/to/cve-watch/dist/cve-watch.js check # exit 1 on findings above thresholdA single-file, zero-dependency CLI (~540 KB unpacked, no runtime deps)
that reads your lockfile, asks the free OSV.dev
database which of your locked packages are known-vulnerable, and exits
non-zero if anything serious is found. It is the missing
npm audit --json that also works for pnpm and yarn lockfiles,
needs no account, and ships one binary.
npm audit only sees npm, and its fix suggestions are uneven.
snyk test is excellent but paid above a free quota. OSV.dev is the
best free multi-ecosystem source of truth on the web (it backs GitHub
Advisories and many others), and it is completely unauthenticated.
cve-watch is a thin, fast, opinionated wrapper around that public
API:
- One CLI, three lockfile formats —
package-lock.json(v1/v2/v3),pnpm-lock.yaml,yarn.lock(classic), plus apackage.jsonfallback. - One HTTP round-trip per scan thanks to OSV.dev's
querybatchendpoint (plus one follow-upgetVulnper unique CVE to hydrate the severity). - CI-shaped by default — non-zero exit on findings, JSON output, severity threshold, ignore list, stateful diff.
- Watch mode — long-running monitor that posts a webhook when a new CVE appears in your lockfile.
- Zero credentials, zero runtime dependencies — the published
tarball is a single self-contained JS file plus the
bin/shim.
# Run from the built bundle (no npm install needed):
node dist/cve-watch.js check # one-shot scan, exit 1 on findings
node dist/cve-watch.js check --severity high # only fail on HIGH or CRITICAL
node dist/cve-watch.js check --json > report.json # machine-readable output
node dist/cve-watch.js watch --interval 600 # poll every 10 minutes
node dist/cve-watch.js explain CVE-2021-23337 # show details for one advisory
# Or link globally so you can type `cve-watch` anywhere:
npm link
# then from your project:
cve-watch checkRunning cve-watch check on the in-repo examples/utils fixture
(lodash, minimist, axios, glob, mkdirp — all pinned to known-vulnerable
versions) produces this real output (no edits, captured on
2026-06-01 against the live OSV.dev API):
$ cve-watch check
cve-watch report
────────────────────────────────────────────────────────────
Dependencies scanned : 16
Vulnerable packages : 3
Total findings : 26
By severity : CRITICAL: 1 HIGH: 10 MEDIUM: 13 LOW: 1
Findings:
CRITICAL minimist@1.2.5
GHSA-xvch-5gv4-984h (CVE-2021-44906)
Prototype Pollution in minimist
HIGH axios@0.21.0
GHSA-3g43-6gmg-66jw (CVE-2026-44495)
axios Vulnerable to Credential Theft and Response Hijacking via Prototype Pollution Gadget in Config Merge
HIGH axios@0.21.0
GHSA-43fc-jf86-j433 (CVE-2026-25639)
Axios is Vulnerable to Denial of Service via __proto__ Key in mergeConfig
HIGH lodash@4.17.20
GHSA-35jh-r3h-8g4q (CVE-2021-23337)
Command Injection in lodash
The process exits with code 1 because findings exist. Drop the
threshold to --severity low to surface everything, or pipe
--json into your dashboard.
The repository ships three fixtures in examples/. Each was scanned
end-to-end with the bundled CLI. Numbers below are the actual
output from cve-watch check against the live OSV.dev API on
2026-06-01.
Setup: lodash@4.17.20, minimist@1.2.5, axios@0.21.0,
glob@7.1.6, mkdirp@0.5.5 — 16 packages in the resolved tree.
$ cve-watch check
…
Dependencies scanned : 16
Vulnerable packages : 3
Total findings : 26
By severity : CRITICAL: 1 HIGH: 10 MEDIUM: 13 LOW: 1
Best wall time: ~1.0 s. The most worrying finding is the
CRITICAL Prototype Pollution in minimist@1.2.5 (CVE-2021-44906).
Setup: express@4.16.0, body-parser@1.18.2, request@2.83.0,
ejs@2.5.5 — 101 packages in the resolved tree.
$ cve-watch check --severity high
…
Dependencies scanned : 101
Vulnerable packages : 8
Total findings : 22
By severity : CRITICAL: 2 HIGH: 8
Findings:
CRITICAL cryptiles@3.2.1
GHSA-rq8g-5pc5-wrhr (CVE-2018-1000620)
Insufficient Entropy in cryptiles
CRITICAL ejs@2.5.5
GHSA-phwq-j96m-2c2q (CVE-2022-29078)
ejs template injection vulnerability
HIGH body-parser@1.18.2
GHSA-qwcr-r2fm-qrc7 (CVE-2024-45590)
body-parser vulnerable to denial of service when url encoding is enabled
Best wall time: ~1.1 s. The two CRITICAL items (ejs template injection, cryptiles entropy) are the kind of finding a security review would block a release for.
Setup: express@^4.21.2, fastify@^5.2.1 — 116 packages in
the resolved tree.
$ cve-watch check
…
Dependencies scanned : 116
Vulnerable packages : 0
Total findings : 0
✓ No vulnerabilities found at or above the configured severity.
Best wall time: ~0.66 s. Zero findings — the green path. The modern fixture is the falsification test for "cve-watch is always noisy": it is not.
cve-watch is designed for cheap, frequent invocation in CI. The
work the tool does is dominated by one or two HTTPS round-trips to
OSV.dev; the in-process parsing and formatting are sub-millisecond
per package.
| fixture | packages | best wall | per-pkg | pkgs/sec |
|---|---|---|---|---|
examples/utils |
16 | ~1.0 s | ~62 ms | ~16 |
examples/modern-app |
116 | ~0.66 s | ~5.7 ms | ~177 |
examples/legacy-app |
101 | ~1.1 s | ~10.5 ms | ~95 |
cve-watch (self) |
309 | ~1.4 s | ~4.5 ms | ~225 |
lodash/lodash lock |
493 | ~2.3 s | ~4.7 ms | ~214 |
The lodash/lodash row is a 493-package lockfile pulled from the
public lodash/lodash@main branch — a real-world upper bound, not a
synthetic test. With --severity high filtering, the wall time is
almost entirely the one querybatch call plus the per-CVE
hydration, which scales with the number of unique CVE IDs, not
the number of packages.
To isolate the in-process cost from the network, scripts/bench.ts
also runs 500 synthetic packages through OsvClient.scan() with a
mocked fetch that simulates an 80 ms round-trip. Numbers below are
real output from npm run bench:
| metric | value |
|---|---|
| packages scanned | 500 |
| runs | 5 |
| min wall time | 81.0 ms |
| median wall time | 81.3 ms |
| p95 wall time | 82.5 ms |
| min per-package time | 0.2 ms |
| throughput (min) | 6.17 k/s |
| HTTP calls / scan (batched) | 1 |
| HTTP calls (naive, 1/pk) | 500 |
The last two rows are the headline. The batched implementation makes
one POST /v1/querybatch call for the whole dependency set
followed by one GET /v1/vulns/{id} per unique CVE; a naive
implementation would do 500 sequential round-trips (~40 s at 80 ms
each) for the same scan.
Create a .cve-watch.json config file in the current directory with
the project's preferred severity threshold, ecosystems, poll interval
and webhook URL. Flags: --severity, --ecosystems (comma list),
--poll-interval (seconds), --webhook. Example output:
Wrote .cve-watch.json
{
"severity": "high",
"ecosystems": ["npm"],
"pollInterval": 3600,
"webhook": null
}
One-shot scan. Reads the lockfile, queries OSV, prints a report
(human or JSON) and exits 1 if at least one finding is at or
above the configured threshold. Flags: --severity <low|medium|high|critical>,
--json, --ecosystem <npm|pnpm|yarn> (override detection),
--ignore <pkg> (repeatable), --save-state. Exit codes: 0 clean,
1 findings, 2 no lockfile, 3 OSV.dev error.
Long-running monitor. Polls on --interval seconds (default 3600),
prints a NEW CVE notification on the terminal and optionally POSTs
a JSON payload to --webhook whenever a CVE that was not present in
the previous tick appears. Flags: --interval, --webhook,
--severity, --ignore, --once (one tick and exit),
--exit-on-first. Use --once in cron or Kubernetes CronJob to get
stateful diffing without keeping a process alive.
Like check, but always exits 0. Designed for human browsing
and for piping into grep/jq. Flags: --severity, --json,
--ecosystem, --ignore.
Compare the current findings with the saved
.cve-watch-state.json. Exits 1 if there are new CVEs (suitable
for CI on PRs), prints resolved CVEs that are no longer present, and
prints the count of unchanged ones. Flags: --json, --save-state.
Fetch a single advisory by its CVE-… or GHSA-… ID and show all
fields OSV.dev has: aliases, severity, summary, details, affected
ranges, references. Flags: --json (raw OSV payload). Exit codes: 0
found, 3 OSV error, 4 unknown ID.
The exit code is the contract: 0 clean, 1 findings at or above
threshold. Pair that with --json and you have a stable gate.
name: Security
on:
push:
branches: [main]
pull_request:
jobs:
cve-watch:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Build cve-watch
run: npm run bundle
working-directory: ./cve-watch
- name: Scan for known CVEs
run: node ./cve-watch/dist/cve-watch.js check --severity high --json > cve-report.json
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: cve-report
path: cve-report.jsonIf you only want the JSON report (and not a failing build), drop the
--severity flag and use cve-watch list --json instead — it always
exits 0.
cve-watch:
image: node:20
stage: test
script:
- npm ci
- npm run bundle --prefix ./cve-watch
- node ./cve-watch/dist/cve-watch.js check --severity high
artifacts:
when: always
paths:
- cve-report.json
reports:
junit: cve-report.jsoncheck exits 1 on findings, so the pipeline fails automatically.
The default reporter uses chalk for severity-coloured output:
CRITICAL is white-on-red, HIGH is red, MEDIUM is yellow, LOW is
blue, UNKNOWN is grey. The header has a 60-column rule, a one-line
severity summary, and a per-finding block with the package, the
advisory ID (with CVE alias in parentheses), and the OSV.dev
one-liner summary. See the Demo section above for a real
example.
--json emits a single, stable JSON document. Schema:
{
"totalDeps": 16,
"vulnerableDeps": 3,
"totalVulns": 26,
"bySeverity": {
"CRITICAL": 1,
"HIGH": 10,
"MEDIUM": 13,
"LOW": 1,
"UNKNOWN": 0
},
"findings": [
{
"dep": { "name": "minimist", "version": "1.2.5" },
"vuln": { "id": "GHSA-xvch-5gv4-984h", "summary": "Prototype Pollution in minimist", "aliases": ["CVE-2021-44906"], "severity": [{"type":"CVSS_V3","score":"CVSS:3.1/…"}] },
"severity": "CRITICAL"
}
],
"warnings": [],
"ignored": []
}cve-watch watch --webhook <url> POSTs one JSON document per new
CVE detected during a tick:
{
"type": "new-cve",
"timestamp": "2026-06-01T10:23:45.123Z",
"package": { "name": "axios", "version": "0.21.0" },
"vuln": {
"id": "GHSA-3g43-6gmg-66jw",
"summary": "axios Vulnerable to Credential Theft …",
"aliases": ["CVE-2026-44495"],
"severity": [{"type":"CVSS_V3","score":"CVSS:3.1/…"}],
"references": [{"type":"WEB","url":"https://github.com/advisories/GHSA-3g43-6gmg-66jw"}]
}
}The shape is stable across minor versions; additive changes only.
| Tool | Auth | Lockfiles | Exit code | JSON | Offline | License | Cost |
|---|---|---|---|---|---|---|---|
npm audit |
none | npm only | 0/1 | yes | no | Artistic | free |
snyk test |
account | multi | 0/1 | yes | no | Apache-2.0 | free < 200 tests/mo, then paid |
osv-scanner |
none | multi + SBOM | 0/1 | yes | yes | Apache-2.0 | free |
| Dependabot (GitHub) | GitHub | many (per ecosystem) | n/a (PR) | via API | no | MIT (UI) | free for OSS |
cve-watch |
none | npm/pnpm/yarn | 0/1/2/3 | yes | no | MIT | free, no quota |
cve-watch does not try to be osv-scanner. The deliberate
trade-offs are: Node-only (the lockfile parsers), no fix-PR
generation, no SBOM import, and no offline mode (yet). In return you
get a single 540 KB binary, zero runtime dependencies, no accounts,
and a CI-shaped exit/JSON contract. If you need multi-ecosystem
support, use osv-scanner; if you need npm-only with no friction,
cve-watch is the lighter tool.
┌──────────────────────────┐
│ CLI (commander) │
└──────────────┬───────────┘
│
┌──────────────┬─────────────┼──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ init │ │ check │ │ watch │ │ list │ │ diff │
└────┬────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Ecosystem + lockfile detection (ecosystem.ts, lockfiles/*.ts) │
└────────────────────────────────┬────────────────────────────────┘
│ list of (name, version, ecosystem)
▼
┌─────────────────────────────────────────────────────────────────┐
│ OsvClient.scan() — 1× querybatch + N× getVuln (osv.ts) │
└────────────────────────────────┬────────────────────────────────┘
│ per-package vuln lists
▼
┌─────────────────────────────────────────────────────────────────┐
│ Severity bucketing, dedup, threshold filter (severity.ts) │
└────────────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────┐ ┌────────────────────────┐
│ Terminal formatter │ │ JSON formatter │ ── reporters
│ (chalk + ora) │ │ (stable schema) │
└───────────────────────┘ └────────────────────────┘
The hot path is OsvClient.scan() → format.ts. Everything else is
stateless glue. State (.cve-watch.json, .cve-watch-state.json) is
two flat JSON files in the project root.
- Ecosystem scope is npm.
pnpm-lock.yamlandyarn.lockparsers exist and ship, but the OSV queries they issue are all against thenpmecosystem. The OSV client is multi-ecosystem ready; PyPI / Go / Rust parsers are a roadmap item, not a v0.2 promise. - No fix suggestions.
cve-watchtells you which version is known-bad and which advisory it triggers; it does not propose a patched version. Usenpm outdated/npx npm-check-updatesfor that. package.json-only mode uses the spec range as a stand-in for the installed version. This can produce false positives if the range includes a vulnerable version that is not actually installed. Always prefer a lockfile.- CVSS gaps. Some OSV advisories have no CVSS score. Those are
reported as
UNKNOWNseverity and will be filtered out by--severity lowby default. The full advisory is still in--jsonoutput. - No proxy or auth settings. OSV.dev is unauthenticated, so
there is no auth to configure. If you need an outbound HTTP
proxy, set the standard
HTTPS_PROXY/HTTP_PROXYenv vars;fetch(Node 18+) honours them. - No offline mode. Every scan hits OSV.dev. For air-gapped use
you need to swap
OsvClientfor a vendored DB; that's on the roadmap.
- Multi-ecosystem parsers — PyPI (
requirements.txt,poetry.lock), Go (go.sum), Rust (Cargo.lock), Maven (pom.xml). - GitHub Action wrapper —
wardsvelds2l/cve-watch-action@v0with first-class annotations on PRs. - Fix-PR generation — propose a minimal-bump PR that resolves the worst finding.
- SBOM export — emit CycloneDX or SPDX alongside the report.
- Offline mode — ship a snapshot of the OSV database and a
--offlineflag for air-gapped environments. - HTML / SARIF reporters — for direct ingestion by GitHub code scanning and similar.
We welcome PRs — see CONTRIBUTING.md for the
setup, the quality gate (npm run all), the Conventional Commits
format, and the lightweight DCO sign-off. The
issue tracker
has a good first issue label for newcomers.
MIT — see the file for the full text.
- OSV.dev and the OSV team for maintaining the public vulnerability database that this project is a thin wrapper around.
- The GitHub Advisory Database for being one of the primary OSV feeders.
- Every contributor who has filed an issue, opened a PR, or sent a reproducer — see the GitHub contributors page for the list.