FairqueryBlogToolsAboutSecurityllms.txt

How do I check npm and PyPI dependencies for vulnerabilities in CI with OSV?

Fairquery ·

Short answer: pin exact versions (use your lockfile or pip freeze), send them to the free OSV.dev API, and fail the build when a vulnerability at or above your severity bar is found. For a free yes/no gate you can call OSV's querybatch endpoint directly or use the free osv_query_packages tool in @fairquery/agent-trust-mcp. For CI-ready rows with CVE IDs, CVSS scores, severity filtering, alias de-duplication and a recommended upgrade version, run the Fairquery OSV Dependency Vulnerability Check Actor: $0.0005 per package checked + $0.0015 per vulnerability found.

Why OSV

OSV.dev is a free, open vulnerability database and API, an open-source project launched by Google. It aggregates advisories from the GitHub Advisory Database, the PyPA advisory database, the Go vulnerability database, RustSec and others in one schema, and covers npm, PyPI, Go, Maven, crates.io, RubyGems, NuGet, Packagist, Pub, Hex and more. It needs no API key. The catch is that a raw OSV response is a list of advisory IDs per package; severity, CVSS, CVE aliases and fixed versions are in separate detail records you must fetch and merge yourself.

Option 1 (free): query OSV directly

curl -s -X POST https://api.osv.dev/v1/querybatch -d '{
  "queries": [
    {"package": {"ecosystem": "npm", "name": "lodash"},  "version": "4.17.20"},
    {"package": {"ecosystem": "npm", "name": "express"}, "version": "4.21.2"}
  ]}'

Real response (October 17, 2026, about 0.2 s): five advisories for lodash 4.17.20, none for express 4.21.2.

{"results":[{"vulns":[{"id":"GHSA-29mw-wpgm-hmr9","modified":"2025-09-29T21:12:31.102523Z"},{"id":"GHSA-35jh-r3h4-6jhm","modified":"2026-10-17T02:55:38.085813Z"},{"id":"GHSA-f23m-r3pf-42rh","modified":"2026-10-17T02:56:13.084362Z"},{"id":"GHSA-r5fr-rjxr-66jc","modified":"2026-10-17T02:56:15.633068Z"},{"id":"GHSA-xxjr-mmjv-4gpg","modified":"2026-10-17T02:55:54.229635Z"}]},{}]}

A minimal free CI gate for an npm lockfile (v2/v3) with jq:

jq '{queries: [.packages | to_entries[] | select(.key | startswith("node_modules/"))
     | {package: {ecosystem: "npm", name: (.key | sub("^.*node_modules/"; ""))}, version: .value.version}]}' \
  package-lock.json > q.json
curl -s -X POST https://api.osv.dev/v1/querybatch -d @q.json > osv.json
jq -e '[.results[].vulns // [] | length] | add == 0' osv.json   # exit code 1 if anything is vulnerable

This gate fails on any advisory, including low-severity ones, because querybatch doesn't return severity. In an agent, the free osv_query_packages tool does the same lookup for up to 200 exact versions per call. Real output for lodash 4.17.20, django 3.2.0 and express 4.21.2 (abridged):

{
  "meta": {
    "vulnerable_count": 2,
    "source": "https://api.osv.dev/v1/querybatch"
  },
  "items": [
    {
      "ecosystem": "npm",
      "name": "lodash",
      "version": "4.17.20",
      "vulnerable": true,
      "vuln_count": 5,
      "vuln_ids": [
        "GHSA-29mw-wpgm-hmr9",
        "GHSA-35jh-r3h4-6jhm",
        "GHSA-f23m-r3pf-42rh",
        "GHSA-r5fr-rjxr-66jc",
        "GHSA-xxjr-mmjv-4gpg"
      ]
    },
    {
      "ecosystem": "PyPI",
      "name": "django",
      "version": "3.2.0",
      "vulnerable": true,
      "vuln_count": 66,
      "vuln_ids": [
        "GHSA-2gwj-7jmv-h26r",
        "GHSA-2hrw-hx67-34x6",
        "GHSA-3h9f-r86x-qvjx",
        "… 63 more"
      ]
    },
    {
      "ecosystem": "npm",
      "name": "express",
      "version": "4.21.2",
      "vulnerable": false,
      "vuln_count": 0,
      "vuln_ids": []
    }
  ]
}

Its sibling osv_get_vulnerability (also free) returns the full advisory, for example GHSA-35jh-r3h4-6jhm = CVE-2021-23337, "Command Injection in lodash", severity HIGH.

Option 2 (API): severity-filtered rows from the OSV Actor

The Actor parses package.json, package-lock.json (v1–v3), requirements.txt or go.mod, queries OSV, fetches every advisory, computes the CVSS v3 base score, maps GHSA/PYSEC/GO IDs to CVEs, collapses duplicate advisories (dedupeAliases), filters by minSeverity, and computes the smallest version that fixes every emitted vulnerability.

We ran it on October 17, 2026 with this input (two explicit packages plus a small package.json):

{
  "packages": [
    {"ecosystem": "npm",  "name": "lodash", "version": "4.17.20"},
    {"ecosystem": "PyPI", "name": "django", "version": "3.2.0"}
  ],
  "manifestText": "{\"dependencies\":{\"lodash\":\"4.17.20\",\"express\":\"4.21.2\"}}",
  "minSeverity": "HIGH"
}

Real output

Abridged: three package rows (lodash appeared in both inputs and was checked once) and the first of 23 vulnerability rows:

{
  "package_rows": [
    {
      "type": "package",
      "ecosystem": "npm",
      "name": "lodash",
      "version": "4.17.20",
      "purl": "pkg:npm/lodash@4.17.20",
      "origin": "input",
      "vulnerable": true,
      "vuln_count": 2,
      "vuln_count_total": 5,
      "vuln_ids": [
        "GHSA-r5fr-rjxr-66jc",
        "GHSA-35jh-r3h4-6jhm"
      ],
      "max_severity": "HIGH",
      "max_cvss_score": 8.1,
      "fixed_versions": [
        "4.18.0",
        "4.17.21"
      ],
      "recommended_version": "4.18.0"
    },
    {
      "type": "package",
      "ecosystem": "PyPI",
      "name": "django",
      "version": "3.2.0",
      "purl": "pkg:pypi/django@3.2.0",
      "origin": "input",
      "vulnerable": true,
      "vuln_count": 21,
      "vuln_count_total": 66,
      "vuln_ids": [
        "GHSA-2gwj-7jmv-h26r",
        "GHSA-p64x-8rxx-wf6q",
        "GHSA-r3xc-prgr-mg9p",
        "… 18 more"
      ],
      "max_severity": "CRITICAL",
      "max_cvss_score": 9.8,
      "fixed_versions": [
        "2.2.28",
        "3.2.13",
        "4.0.4",
        "… 47 more"
      ],
      "recommended_version": "5.2.17"
    },
    {
      "type": "package",
      "ecosystem": "npm",
      "name": "express",
      "version": "4.21.2",
      "purl": "pkg:npm/express@4.21.2",
      "origin": "manifest",
      "vulnerable": false,
      "vuln_count": 0,
      "vuln_count_total": 0,
      "vuln_ids": [],
      "max_severity": "NONE",
      "max_cvss_score": null,
      "fixed_versions": [],
      "recommended_version": null
    }
  ],
  "first_vulnerability_row": {
    "type": "vulnerability",
    "id": "GHSA-r5fr-rjxr-66jc",
    "package_name": "lodash",
    "package_version": "4.17.20",
    "cve_ids": [
      "CVE-2026-4800"
    ],
    "summary": "lodash vulnerable to Code Injection via `_.template` imports key names",
    "severity": "HIGH",
    "cvss_score": 8.1,
    "cvss_version": "3.1",
    "fixed_versions": [
      "4.18.0"
    ],
    "osv_url": "https://osv.dev/vulnerability/GHSA-r5fr-rjxr-66jc"
  }
}

Because a package.json holds ranges, the Actor also emitted a free warning row; feed it the lockfile instead for exact versions:

{
  "type": "error",
  "code": "RANGE_APPROXIMATED",
  "message": "package.json ranges were resolved to their lower bound (e.g. ^4.17.20 -> 4.17.20). Installed versions may differ; prefer package-lock.json.",
  "context": {
    "manifest_type": "package.json",
    "severity": "warning"
  }
}

And the counts from the run's RUN_SUMMARY. OSV knows 66 advisories for django 3.2.0; after the HIGH filter and alias de-duplication 21 remain:

{
  "packages_checked": 3,
  "packages_vulnerable": 2,
  "vulnerabilities_emitted": 23,
  "vulnerabilities_filtered_out": 20,
  "vulnerabilities_deduplicated": 28,
  "severity_counts": {
    "NONE": 0,
    "UNKNOWN": 0,
    "LOW": 0,
    "MEDIUM": 0,
    "HIGH": 17,
    "CRITICAL": 6
  },
  "osv_requests": 72
}

The run took 2.8 seconds on Apify and would cost 3 × $0.0005 + 23 × $0.0015 = $0.036. The README documents about 5–10 s for a 2-package run; time grows with the number of advisories to fetch (8 detail requests run in parallel).

GitHub Actions: fail the PR on HIGH/CRITICAL

Add your Apify token as the repository secret APIFY_TOKEN, then:

name: dependency-vulnerabilities
on: [pull_request]
jobs:
  osv:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: OSV check (HIGH and CRITICAL only)
        env:
          APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
        run: |
          jq -n --rawfile m package-lock.json \
            '{manifestText: $m, minSeverity: "HIGH"}' > input.json
          curl -sf -X POST \
            "https://api.apify.com/v2/acts/fairquery~osv-dependency-vulnerability-check/run-sync-get-dataset-items" \
            -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
            -d @input.json > osv.json
          jq -r '.[] | select(.type=="package" and .vulnerable)
                 | "\(.name)@\(.version) -> upgrade to \(.recommended_version // "n/a") (\(.max_severity))"' osv.json
          n=$(jq '[.[] | select(.type=="vulnerability")] | length' osv.json)
          echo "$n HIGH/CRITICAL vulnerabilities"
          test "$n" -eq 0

Run against the dataset from our test run above, the reporting and gating steps print:

lodash@4.17.20 -> upgrade to 4.18.0 (HIGH)
django@3.2.0 -> upgrade to 5.2.17 (CRITICAL)
23 HIGH/CRITICAL vulnerabilities

and exit with status 1, failing the job. Notes: when you pass only a manifest, only the packages in that manifest are checked and billed; an input with neither packages nor manifestText fails immediately with an INPUT_EMPTY error row and no package or vulnerability events are charged. For Python, use pip freeze > requirements.txt first (unpinned lines are skipped with a VERSION_NOT_PINNED warning). The default safety cap is maxPackages: 1000 unique packages per run; raise it for very large lockfiles. You can also set a maximum charge per run in the Apify run options; the Actor stops cleanly at the limit. Prefer n8n? Fairquery's free n8n templates include an OSV CI gate workflow that calls this Actor.

Pricing

OptionPrice
OSV.dev API directly, osv_query_packages, osv_get_vulnerabilityFree
OSV Actor / osv_audit_dependencies MCP tool$0.0005 per package row + $0.0015 per vulnerability row emitted (after the severity filter and de-duplication), + Apify's $0.00005 Actor-start event. Error and warning rows are never charged.

Cost scales with what you check: a 500-package lockfile with no HIGH+ findings costs 500 × $0.0005 = $0.25 per run. For a gate that runs on every push, consider the free lookup for the pass/fail decision and the Actor for the detailed report.

How it compares with alternatives

OptionGood forTrade-offs
npm auditBuilt into npm, free, uses the GitHub Advisory Databasenpm projects only
pip-auditFree PyPA tool for Python environments and requirements filesPython only
OSV-ScannerFree, open-source CLI from Google that scans lockfiles and directories against OSV, many ecosystemsA binary to install and run in CI; output formatting and gating are up to you
Dependabot / GitHub security alertsAutomatic alerts and update PRs on GitHub repositoriesWorks on repositories hosted on GitHub; not built for checking an arbitrary package list from an agent or another CI system
Fairquery OSV ActorOne API/MCP call from CI, agents or n8n; CVSS, CVE mapping, severity filter, de-duplication, recommended version as JSON rowsPaid per package and per vulnerability; exact versions only; no reachability analysis, license checks or container image scanning

FAQ

How do I fail a CI build when a dependency has a known vulnerability?

Pin exact versions (lockfile or pip freeze), query OSV.dev, and exit non-zero when results contain vulnerabilities at or above your severity bar. The post shows a free jq + OSV querybatch gate and a GitHub Actions job using the Fairquery OSV Actor with minSeverity HIGH.

Is the OSV.dev API free?

Yes. OSV.dev is free and needs no API key. Its querybatch endpoint returns advisory IDs per package; severity and fix details come from separate per-advisory requests.

What does the Fairquery OSV Actor add over the raw OSV API?

Manifest parsing (package.json, package-lock.json v1-v3, requirements.txt, go.mod), computed CVSS v3 scores, CVE mapping, a minSeverity filter, alias de-duplication and a recommended upgrade version, as JSON rows for CI or agents.

How much does it cost?

$0.0005 per package checked plus $0.0015 per vulnerability row emitted after filtering and de-duplication. Error and warning rows are free. Our test (3 packages, 23 HIGH/CRITICAL vulnerabilities) would cost $0.036.

Which ecosystems are supported?

Everything OSV supports, including npm, PyPI, Go, Maven, crates.io, RubyGems, NuGet, Packagist, Pub and Hex. Exact versions are required; package.json ranges are approximated to their lower bound with a warning.

Does no result mean my dependencies are safe?

No. It means OSV has no matching advisory. No database is complete, and the check does not cover reachability, licenses or container images.

Honest limits: data comes from OSV.dev and its upstream databases. No advisory database is complete, and no results does not prove a package is safe.

Source and docs: github.com/fairquery/osv-dependency-vulnerability-check (MIT) · Apify Store · Fairquery OSV for VS Code · all Fairquery tools