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 schedule trigger re-scans main.
  • Download, then run the installer. The installer verifies the binary against the published SHA256SUMS before installing, and installs where you point DYNV_INSTALL_DIR; our docs recommend this form over piping into a shell (CI docs). We ran this installer on 2026-11-01 and the dynv scan it installed gave the results shown below.
  • npm ci --ignore-scripts installs 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; set DYNV_INSTALL_DIR to 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). --system also 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 scan yet. The JSON has the fields but they are empty today; check exploitation status separately.
  • The GitHub Action is not published. integrations/github-action exists in our repository but there is no uses: line to copy yet (CI docs); the shell steps above are the supported way today.

Facts last verified . Spotted an error? Tell us.

New here? Start here for the guides in a sensible order.