[ blog · tutorial ]9 min read

A Dependency Vulnerability Scanning API for AI Agents: OSV.dev, CVSS Scores, and Fix Versions in One Call

Sarah ChoyPublished September 1, 20269 min read
A Dependency Vulnerability Scanning API for AI Agents: OSV.dev, CVSS Scores, and Fix Versions in One Call

OSV.dev is the best free vulnerability database in open source, and its API gives you everything except the one field you need to make a decision: the number. Here's how to turn advisories into a threshold you can branch on.

TL;DR

  • OSV.dev aggregates the GitHub Advisory Database, Go vuln DB, RustSec, PyPA, and distro trackers into one CC-BY-4.0 schema — the right source for open-source dependency data.
  • OSV ships the CVSS vector string but not the score. A build gate needs a number, so this endpoint computes the CVSS v3.1 base score from the vector using the official formula.
  • One POST checks up to 50 packages across 13 ecosystems and costs 5 credits flat — 0.1 credits per package at a full batch, charged only on success.
  • Send exact installed versions from your lockfile, never ranges: OSV matches ranges against a concrete version, so a range as input has no single answer.
  • A newer version is not automatically a clean one. lodash 4.17.21 still carries advisories fixed only in 4.18.0 — which is exactly why you check rather than assume.

The field that is missing from every advisory feed

OSV.dev is the best thing that has happened to open-source vulnerability data. Google built it to solve a real mess: the GitHub Advisory Database, the Go vulnerability database, RustSec, PyPA, and the security trackers of every major Linux distribution each described the same class of problem in a different schema, with different version semantics. OSV normalizes all of it, publishes under CC-BY-4.0, and answers in milliseconds without an API key.

It also does not tell you how bad anything is. An OSV advisory carries the CVSS vector string — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — but not the score that vector produces. So every team that wants a rule like "fail the build above 7.0" ends up implementing the CVSS specification themselves, or gives up and matches on severity strings instead.

That is a small gap with a large consequence, because a severity label is not a threshold. "HIGH" is not a number you can compare against.

What the endpoint returns

Dependency Vulnerability Scan takes up to 50 packages in one POST and returns, per package, every non-withdrawn advisory with its CVE alias, a computed CVSS v3.1 base score, CWE weakness ids, and the exact versions that resolve it — plus a rollup of counts by severity across the whole batch.

import httpx, os
API = "https://www.apipick.com/api"
HEADERS = {"x-api-key": os.environ["APIPICK_KEY"]}

def scan(packages):
    r = httpx.post(f"{API}/scan-dependencies",
                   headers=HEADERS, json={"packages": packages})
    r.raise_for_status()
    return r.json()

report = scan([
    {"ecosystem": "npm",  "name": "lodash",   "version": "4.17.15"},
    {"ecosystem": "PyPI", "name": "requests", "version": "2.19.0"},
])

print(report["summary"])
# {'packages_scanned': 2, 'vulnerable_packages': 2,
#  'total_vulnerabilities': 16, 'highest_cvss_score': 8.1,
#  'by_severity': {'critical': 0, 'high': 5, 'medium': 9, ...}}

Gating a release on a number

Because the score is numeric, the policy is one comparison. Note the explicit None handling: an advisory with no v3 vector has a null score, and coercing that to zero is how an unscored critical slips past a gate.

THRESHOLD = 7.0

blocking = [
    (pkg["name"], v)
    for pkg in report["results"]
    for v in pkg["vulnerabilities"]
    if v["cvss_score"] is not None and v["cvss_score"] >= THRESHOLD
    or v["cvss_score"] is None and v["severity"] in ("HIGH", "CRITICAL")
]

if blocking:
    for name, v in blocking:
        fix = ", ".join(v["fixed_versions"]) or "no fix published"
        print(f"{name}: {v['id']} {v['severity']} {v['cvss_score']} -> {fix}")
    raise SystemExit(1)

The coding-agent case

The more interesting consumer is not CI. It is the agent writing the manifest in the first place. A coding agent that can call this endpoint checks a version before it commits to it, and proposes the fixed version in the same turn instead of producing a dependency bump that a scanner rejects an hour later in CI.

The tool definition is one object with three fields, and the schema is served at GET /api/scan-dependencies/tool-schema in both OpenAI and Anthropic formats, so you can fetch it rather than hand-write it. Pair it with Cybersecurity Search when the agent needs the wider story — KEV status, EPSS probability, vendor advisories — for a CVE it just found.

A newer version is not a clean version

It is tempting to treat "upgrade to latest" as the fix and move on. Scan lodash@4.17.21 — the version most people think of as the safe one — and advisories still come back, fixed only in a 4.18.0 that does not exist yet. That is not a bug in the data; it is the data doing its job. The habit worth building is to check the version you are actually shipping rather than the version you assume is fine.

Build it yourself, or call it

Wire OSV yourselfAPI Pick
Batch behaviourquerybatch returns ids only — second round trip per advisoryOne call, full advisory bodies
CVSS scoreImplement the v3.1 spec yourselfComputed and returned
Fix versionsFilter the affected-ranges matrix per packageResolved per package
Partial failureOne bad package sinks the batchReported inline, rest still returns
Ecosystem namesCase-sensitive: PyPI, crates.io, not pypiCommon aliases normalized
CostFree, plus the code you maintain5 credits/call, only on success

Where the data comes from

Advisories are OSV.dev's, published under CC-BY-4.0, covering npm, PyPI, Go, Maven, NuGet, RubyGems, crates.io, Packagist, Hex, Pub, CRAN, SwiftURL, and ConanCenter. We add the score computation, the per-package rollup, and the resilience: a single package that fails to look up comes back with an error field while every other package reports normally, and the response sets packages_failed so you know the summary is incomplete rather than clean.

A free key comes with 100 credits and no card, which is twenty full 50-package scans. Point a CI job or an agent at it and see what your lockfile has been carrying.

Frequently Asked Questions

How is this different from npm audit or a GitHub Dependabot alert?

Those are tied to one ecosystem and one workflow. npm audit only knows npm, and Dependabot files pull requests against repositories it watches. This is a plain HTTP endpoint that answers a question about any package in 13 ecosystems from anywhere — a CI script, a coding agent mid-conversation, a vendor-review spreadsheet, a Slack bot. It does not open PRs or manage your repo; it answers 'is this exact version affected, and what fixes it' so your own tooling can decide what to do.

Why compute the CVSS score instead of returning what OSV gives you?

OSV advisories carry the CVSS vector string, like CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H, but not the resulting number. A policy such as 'fail the build above 7.0' needs the number, so every consumer ends up implementing the same specification. We compute the CVSS v3.1 base score from the vector using the official formula and return it as cvss_score, so your gate is a numeric comparison rather than a string match against severity labels.

What happens when an advisory has no CVSS v3 vector?

Some advisories carry only a v4 vector, and some ecosystem advisories ship no vector at all. In those cases cvss_score is null and the severity field falls back to the publisher's own qualitative rating, or UNKNOWN if there is none. Write your gate to treat a null score explicitly rather than coercing it to zero, otherwise an unscored critical advisory sails through.

Can I send a version range like ^4.17.0?

No. Send the exact installed version as it appears in your lockfile. OSV decides whether a version is affected by matching it against the advisory's affected ranges, so it needs one concrete point to test. A range as input has no single answer — 4.17.0 and 4.17.21 can land on opposite sides of the same advisory.

What does it cost to scan a large project?

One call is 5 credits no matter how many packages it carries, up to the 50-package cap. A 200-package lockfile is four calls, or 20 credits. Credits are deducted only on a successful response, so an OSV outage or a malformed request costs nothing.

APIs used in this article

Sarah Choy
Written by
Sarah Choy
CEO, API Pick

Sarah Choy is the CEO of API Pick. She writes about building production-ready APIs for AI agents and LLM workflows.