Skip to content
wardsvelds2lPublic

About

Fast, no-auth CVE scanner for Node.js. Free OSV.dev integration, single binary, CI gate.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

cve-watch

Fast, no-auth CVE scanner for Node.js projects. Free OSV.dev integration. Works as CLI and CI gate.

CI npm version npm downloads codecov License: MIT Node >= 20

# 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 threshold

A 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.


Why

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 a package.json fallback.
  • One HTTP round-trip per scan thanks to OSV.dev's querybatch endpoint (plus one follow-up getVuln per 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.

Quick start

# 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 check

Demo

Running 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.

Real-world findings

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.

examples/utils — mixed utility packages

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).

examples/legacy-app — Express 4.16 + request + ejs

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.

examples/modern-app — current Express + Fastify

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.

Performance

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.

Live scans (real OSV.dev, 3 runs each, take the best)

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.

Stress test (synthetic, mocked OSV)

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.

Commands

cve-watch init

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
}

cve-watch check

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.

cve-watch watch

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.

cve-watch list

Like check, but always exits 0. Designed for human browsing and for piping into grep/jq. Flags: --severity, --json, --ecosystem, --ignore.

cve-watch diff

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.

cve-watch explain <CVE|GHSA>

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.

CI integration

The exit code is the contract: 0 clean, 1 findings at or above threshold. Pair that with --json and you have a stable gate.

GitHub Actions

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.json

If 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.

GitLab CI

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.json

check exits 1 on findings, so the pipeline fails automatically.

Output formats

Terminal (default)

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 (--json)

--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": []
}

Webhook payload (used by watch)

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.

Comparison

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.

Architecture

                      ┌──────────────────────────┐
                      │       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.

Limitations

  • Ecosystem scope is npm. pnpm-lock.yaml and yarn.lock parsers exist and ship, but the OSV queries they issue are all against the npm ecosystem. The OSV client is multi-ecosystem ready; PyPI / Go / Rust parsers are a roadmap item, not a v0.2 promise.
  • No fix suggestions. cve-watch tells you which version is known-bad and which advisory it triggers; it does not propose a patched version. Use npm outdated / npx npm-check-updates for 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 UNKNOWN severity and will be filtered out by --severity low by default. The full advisory is still in --json output.
  • 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_PROXY env vars; fetch (Node 18+) honours them.
  • No offline mode. Every scan hits OSV.dev. For air-gapped use you need to swap OsvClient for a vendored DB; that's on the roadmap.

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@v0 with 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 --offline flag for air-gapped environments.
  • HTML / SARIF reporters — for direct ingestion by GitHub code scanning and similar.

Contributing

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.

License

MIT — see the file for the full text.

Acknowledgments

  • 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.

About

Fast, no-auth CVE scanner for Node.js. Free OSV.dev integration, single binary, CI gate.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages