Fail your CI build on known vulnerable dependencies (no account needed)
Short answer: install the dynv CLI in the job and run dynv scan --fail-on high after your dependencies are installed. It matches what it finds against OSV.dev, prints a table, and exits 1 if anything is at or above the threshold, 0 if clean, 2 on an error. It needs no account, token or Dynv server.
A gate in CI catches known-vulnerable dependencies before they ship. This page gives a workflow you can copy, how thresholds and exit codes behave, a small script for accepted exceptions, and the limits. Everything here was run with the current CLI on 2026-11-01, including installing it with the published installer.
The workflow
# .github/workflows/vulnerabilities.yml
name: vulnerabilities
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "17 6 * * *"
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
env:
DYNV_INSTALL_DIR: ${{ runner.temp }}/dynv-bin
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci --ignore-scripts
- name: Install dynv
run: |
curl -fsSL https://dynv.dev/install.sh -o "$RUNNER_TEMP/dynv-install.sh"
sh "$RUNNER_TEMP/dynv-install.sh"
echo "$DYNV_INSTALL_DIR" >> "$GITHUB_PATH"
- name: Fail on known vulnerabilities
run: dynv scan --fail-on high
Notes on choices:
- Scheduled run. A dependency that was clean when you merged can get an advisory tomorrow. The nightly
scheduletrigger re-scansmain. - Download, then run the installer. The installer verifies the binary against the published
SHA256SUMSbefore installing, and installs where you pointDYNV_INSTALL_DIR; our docs recommend this form over piping into a shell (CI docs). We ran this installer on 2026-11-01 and thedynv scanit installed gave the results shown below. npm ci --ignore-scriptsinstalls from the lockfile without running package scripts, which is safer for a job that doesn’t need them (why). Drop the flag if your project needs build scripts.- GitLab: the same two commands work in
before_script/script; setDYNV_INSTALL_DIRto a path inside the workspace. - Other ecosystems. The scan reads lockfiles and installed packages for npm (including pnpm and yarn lockfiles), PyPI, Go, crates.io, RubyGems and Packagist; install your dependencies the usual way first (supported inputs).
--systemalso reads OS package databases, for scanning images rather than repositories.
Thresholds and exit codes
On a project locked to [email protected]:
$ dynv scan
SEVERITY PACKAGE VERSION FIXED ID PATH
high lodash 4.17.15 4.17.21 GHSA-35jh-r3h4-6jhm direct
high lodash 4.17.15 4.17.19 GHSA-p6mc-m468-83gw direct
high lodash 4.17.15 4.18.0 GHSA-r5fr-rjxr-66jc direct
medium lodash 4.17.15 4.17.21 GHSA-29mw-wpgm-hmr9 direct
medium lodash 4.17.15 4.18.0 GHSA-f23m-r3pf-42rh direct
medium lodash 4.17.15 4.17.23 GHSA-xxjr-mmjv-4gpg direct
6 vulnerabilities (3 high, 3 medium) in 1 packages; 6 fixable.
| Command | Exit code on that project |
|---|---|
dynv scan (default --fail-on high) |
1 |
dynv scan --fail-on critical |
0 |
dynv scan --fail-on none |
0 (report only) |
dynv scan --osv-url http://127.0.0.1:9 (unreachable) |
2, with the error on stderr |
| a clean project | 0, “No known vulnerabilities in 1 packages” |
Pick the threshold by how much friction your team can absorb. A common progression: start at critical, clean up, then tighten to high. Exit code 2 means the scan itself failed, which you usually want to treat as a failure, not a pass. Lockfile parse errors or a truncated inventory also exit 2 unless you pass --allow-partial.
Accepting a known exception
dynv scan has no ignore file today. When you have reviewed a finding and decided it doesn’t apply, gate on the JSON yourself. This script (tested on the project above) blocks on high or critical findings except advisories you list:
// gate.mjs
import { readFileSync } from 'node:fs';
const accepted = new Set((process.env.ACCEPTED ?? '').split(',').filter(Boolean));
const report = JSON.parse(readFileSync(0, 'utf8'));
const blocking = report.findings.filter((f) => ['critical', 'high'].includes(f.severity) && !accepted.has(f.id));
for (const f of blocking) console.error(`${f.severity} ${f.id} ${f.package}@${f.version} -> ${f.fixed ?? 'no fix'}`);
process.exit(blocking.length ? 1 : 0);
dynv scan --json --fail-on none | ACCEPTED=GHSA-xxxx-xxxx-xxxx node gate.mjs
Without ACCEPTED it exited 1 and printed the three high findings; with all three listed it exited 0. Keep the accepted list in the repository with a reason and a date, and review it. An exception with no expiry is how a gate rots. Each finding in the JSON also includes id, aliases, severity, package, version, fixed, direct, source and guidance.
What leaves the runner
dynv scan sends nothing to Dynv. It does query OSV.dev, and what it sends is the ecosystem, name and version of each package, nothing else. We pointed it at a local stand-in for OSV to see exactly what a lockfile with [email protected] and a private @acme/[email protected] sends:
POST /v1/querybatch (User-Agent: osv-client)
query: {"package":{"ecosystem":"npm","name":"@acme/internal-billing"},"version":"2.3.0"}
query: {"package":{"ecosystem":"npm","name":"lodash"},"version":"4.17.21"}
Your private package names are in there. Set DYNV_VULNS_PRIVATE='npm:@acme/*' and the private one is not sent (we confirmed only the lodash query remained). For no outbound traffic at all, --offline --advisories DIR matches against a local directory of OSV records, and --osv-url/DYNV_OSV_URL points the scan at a mirror or proxy (docs).
How it compares
| What it is | Strength | Gap | |
|---|---|---|---|
dynv scan |
One command, many ecosystems, no account | Simple exit-code gate; can also scan images with --system; offline mode |
No PR-diff mode, no ignore file, no SARIF output yet |
| OSV-Scanner | Google’s officially supported OSV frontend, CLI or Go library | Reusable GitHub workflows, including a PR check that reports only vulnerabilities introduced by the pull request | More setup; its own flags and config |
npm audit |
Built into npm | Zero install | npm only; uses npm’s own advisory data |
| Dependabot | GitHub’s alerts and update PRs | Opens the fix PR | A queue, not a gate |
These are complements. A gate stops new problems; update bots reduce the backlog.
Limits
- It reads what’s in the lockfile or the installed tree, not what’s deployed. A fixed lockfile with an old container in production still looks clean in CI. For what your running services have installed, use vulnerability monitoring.
- A match is a known advisory for that version, not proof you’re exploitable. The advisory’s conditions decide that (how to triage).
- KEV and EPSS data aren’t shown by
dynv scanyet. The JSON has the fields but they are empty today; check exploitation status separately. - The GitHub Action is not published.
integrations/github-actionexists in our repository but there is nouses:line to copy yet (CI docs); the shell steps above are the supported way today.
Facts last verified . Spotted an error? Tell us.