Library Audit: from a PURL to vulnerabilities and compromises

This post thumbnail

A lot of the packages compromised on npm and PyPI this year never got a CVE.

When LiteLLM versions 1.82.7 and 1.82.8 were published to PyPI on March 24 carrying a credential harvester and Kubernetes lateral-movement toolkit, they were quarantined within hours. The fix wasn't a patch — it was a rollback to 1.82.6 and a GitHub Security Advisory (GHSA-5mg7-485q-xm76). No CVE was issued. Same pattern with @bitwarden/[email protected], the SAP CAP "Mini Shai-Hulud" cluster, the PyTorch Lightning 2.6.2/2.6.3 PyPI compromise, and the dozens of npm worm victims in between. The package gets yanked, an advisory ships through OSV or GHSA, and the CVE process never enters the picture.

That's the gap Library Audit is built for. POST a list of PURLs, get back the ones that map to a known vulnerability or a known compromise — pulled from the same OSV, GHSA, Snyk, and vendor advisory feeds.

You can call it from a pre-commit hook, a code review comment, a Slack bot that reacts to "should I add this?", a CI step that scans the diff in package.json, or a one-line curl from a developer's terminal. Anywhere a PURL is the cheapest thing to produce.


One call, real packages, real compromise

Mixing CVE-tracked vulnerabilities and registry-yanked malware in the same request:

curl -X POST https://vulners.com/api/v4/audit/library \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"packages": [
"pkg:pypi/[email protected]",
"pkg:pypi/[email protected]",
"pkg:npm/%40bitwarden/[email protected]",
"pkg:pypi/[email protected]"
],
"cvelist_metrics": true
}'
| python3 -m json.tool

Three entries are registry-yanked malware, one is a requests version most people would assume is fine. The response sorts the noisiest packages to the top — and there are no clean entries:

{
"result": {
"totalPackages": 4,
"errors": {},
"issues": [
{
"package": "pkg:pypi/[email protected]",
"version": "1.82.7",
"fixedVersion": "1.83.10",
"applicableAdvisories": [
{
"id": "OSV:MAL-2026-2144", "match": ">=1,<=1.82.8", "registry": "pypi", "cvelistMetrics": []
},
/* OSV:GHSA-5MG7-485Q-XM76 and OSV:PYSEC-2026-2 also omitted —
both carry cvelistMetrics: [] (no CVE assigned) */

{
"id": "OSV:GHSA-R75F-5X8P-QVMC", "match": ">=1.81.16,<1.83.7", "registry": "pypi",
"cvelistMetrics": [
{
"cve": "CVE-2026-42208",
"cvss": {
"score": 9.8, "severity": "CRITICAL", "version": "3.1",
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"source": "nvd"
},
"epss": [
{ "cve": "CVE-2026-42208", "epss": 0.43191, "percentile": 0.9754, "date": "2026-05-15" }
],
"exploitation": {
"wildExploited": true,
"wildExploitedSources": [
{ "type": "cisa_kev", "idList": ["CISA-KEV-CVE-2026-42208"], "firstSeen": "2026-05-08T00:00:00" },
{ "type": "vulncheck_kev", "idList": ["VULNCHECK-KEV:CVE-2026-42208"], "firstSeen": "2026-04-27T00:00:00" },
{ "type": "cisa", "idList": ["CISA:41AA7DE74300E9..."], "firstSeen": "2026-05-08T12:00:00" }
]
},
"ai_score": { "value": 6.0, "uncertainty": 2.1 },
"published": "2026-05-08T03:38:14"
}
]
}
/* 14 more advisories omitted — additional OSV:GHSA-* and SNYK:PYTHON-LITELLM-*
entries covering auth, SSRF, and other LiteLLM CVEs through 1.83.x */

]
},
{
"package": "pkg:pypi/[email protected]",
"version": "2.31.0",
"fixedVersion": "2.33.0",
"applicableAdvisories": [
{
"id": "OSV:GHSA-9HJG-9R4M-MVJ7", "match": "<2.32.4", "registry": "pypi",
"cvelistMetrics": [
{
"cve": "CVE-2024-47081",
"cvss": { "score": 5.3, "severity": "MEDIUM", "version": "3.1", "source": "[email protected]" },
"epss": [{ "cve": "CVE-2024-47081", "epss": 0.00208, "percentile": 0.43109, "date": "2026-05-17" }],
"exploitation": { "wildExploited": false, "wildExploitedSources": [] },
"ai_score": { "value": 6.9, "uncertainty": 1.5 }
}
]
}
/* 4 more advisories omitted */
]
},
{
"package": "pkg:npm/%40bitwarden/[email protected]",
"version": "2026.4.0",
"applicableAdvisories": [
{ "id": "OSV:MAL-2026-3020", "match": ">=2026,<=2026.4.0", "registry": "npm", "cvelistMetrics": [] },
{ "id": "SNYK:JS-BITWARDENCLI-16150367", "match": "=2026.4.0", "registry": "npm", "cvelistMetrics": [] }
]
},
{
"package": "pkg:pypi/[email protected]",
"version": "2.6.2",
"fixedVersion": ">2.6.3",
"applicableAdvisories": [
{ "id": "OSV:MAL-2026-3201", "match": ">=2,<=2.6.3", "registry": "pypi", "cvelistMetrics": [] }
]
}
]
}
}

A few things worth noticing.

OSV:MAL-* is the marker for the "no CVE" path — that's the OSV malicious-package advisory family. Three of the four packages here have one (LiteLLM, Bitwarden CLI, PyTorch Lightning) and none of those three have a CVE attached. The same response also carries OSV:GHSA-*, OSV:PYSEC-*, and SNYK:* records — Library Audit pulls from all of them in parallel, so a single PURL lookup catches both the registry-yank advisory and any CVE-tracked issues the package has accumulated.

cvelist_metrics: true lights up the prioritization stack. Every CVE-mapped advisory carries CVSS (score, severity, vector), EPSS (with percentile and date), the Vulners AI Score (value + uncertainty), and a wildExploited flag sourced from CISA, CISA KEV, VulnCheck KEV, AttackerKB, and CIRCL in parallel. The standout in this response is LiteLLM 1.82.7 — beyond being the malicious release, it also matches CVE-2026-42208 (CVSS 9.8, currently in CISA KEV and exploited in the wild). The pre-install check catches both signals in one call: don't install this version because it was compromised, and don't install this version because it has a critical actively-exploited CVE either. OSV:MAL-* records return empty cvelistMetrics: [] — they're CVE-less by design, which is itself useful signal.

The "clean" package wasn't clean. [email protected] came back with five advisories of its own. In a real audit, there's no such thing as a clean input — every package version carries some accumulated debt. That's the value of running this check before you install: if even the boring library has five known issues, the unknown one absolutely deserves a check.

fixedVersion is the latest known-clean release, not just the next patch after the worst finding. For LiteLLM it's 1.83.10 — the version that resolves all 17 applicable advisories, not just the malicious-package one. For Bitwarden CLI it's 2026.4.0 — the same number as the input, because the malicious release was yanked and the version was republished clean (the match range still flags the compromised build). For Lightning it's >2.6.3, a range form.

The advisory id is whatever OSV, GHSA, or a vendor feed assigned to it; the match is the vulnerable or compromised range; registry tells you which ecosystem the advisory targets. For OS packages, distro and arch appear when the advisory is scoped that way. For Maven, classifier shows up when the advisory targets one. Fields that don't apply are omitted from the JSON — they aren't returned as null — so parsing stays simple.

result.errors carries any PURLs that failed to parse, keyed by their 0-based index in your input. Library Audit doesn't fail the call when some entries are malformed — it returns the issues it could compute and surfaces the rest for you to fix. Pipelines stay green.


When to reach for it

The Vulners Audit family has three entry points for dependency data, and the choice is about how structured your input is:

  • Library Audit — you have PURLs, or you can spell one. Mixed ecosystems in one call. Best for pre-install checks, code-review automation, agent tool-calls, asset databases, and anywhere the source isn't a clean lockfile or SBOM.
  • Package Audit — you have raw lockfile content. POST a pip freeze, a package-lock.json, a poetry.lock, a uv.lock, or a go list -m all and Vulners parses it server-side.
  • SBOM Audit — you already have an SPDX or CycloneDX JSON. Upload it, get the full report.

The matching engine and the malicious-package coverage are the same across all three. The choice is what input you happen to have in your hand.


Try it

The endpoint is documented in the Audit API reference. It's available on the free tier — the cvelist_metrics: true flag used in the example above is the paid-plan addition, attaching per-CVE EPSS, CVSS, exploitation status, and AI Score to each advisory.

If you've ever paused before npm install something-i-just-heard-about, this is the API for that pause. PURLs in, fixes out — including the compromises that never make it to NVD.