CVE Matching
List the OSV advisories filed under a package name, alongside the versions nixpkgs carries for it, with a confidence score per advisory.
GET /api/v1/vulns/:packagePath Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
package | string | Yes | nixpkgs attribute or package name. Letters, digits, ., _, / and - only; anything else answers 400. Package-set prefixes such as python3Packages are stripped before lookup |
There are no query parameters. A query string is not read: ?version=8.4.0 returns the same body as no query at all. Version comparison uses the nixpkgs-unstable version this endpoint looks up itself, never a version supplied by the caller.
Response
A live response for python3Packages.requests:
{
"success": true,
"data": {
"package": "python3Packages.requests",
"normalizedName": "requests",
"nixpkgsVersions": {
"nixos-25.11": { "version": "2.31.0", "status": "outdated", "vulnerable": null },
"nixos-26.05": { "version": "2.32.3", "status": "newest", "vulnerable": null },
"nixpkgs-unstable": { "version": "2.32.3", "status": "newest", "vulnerable": null }
},
"newestVersion": "2.32.3",
"versionSource": "nixos-search",
"truncated": false,
"vulnerabilities": [
{
"matches": [{ "product": "requests", "ecosystem": "PyPI", "ranges": [{ "introduced": "0", "fixed": "2.32.0" }], "cveId": "CVE-2024-35195" }],
"confidence": { "score": 80, "factors": ["exact_name_match", "version_in_vulnerable_range"], "level": "high" },
"cveId": "CVE-2024-35195",
"source": "osv",
"ecosystem": "PyPI",
"osvId": "GHSA-9wx4-h78v-vm56",
"summary": "Requests Session object leaks proxy credentials",
"severity": [{ "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N" }],
"references": [{ "type": "ADVISORY", "url": "https://github.com/advisories/GHSA-9wx4-h78v-vm56" }]
}
],
"totalVulnerabilities": 1,
"highConfidenceCount": 1,
"ecosystemsQueried": ["PyPI"]
},
"timestamp": "2026-10-21T19:51:47.988Z"
}requests matches the python3Packages. naming segment, so this request queried PyPI and the advisory above is a real match for the Python package. A C library like curl or openssl matches no naming segment: OSV is never queried by ecosystem for it ("ecosystemsQueried": []), but the endpoint does not stop there -- it additionally checks the same advisory index the weekly digest uses (see What is queried), so vulnerabilities/totalVulnerabilities can still be non-empty for such a package, carrying "ecosystemFallback": true and lower-confidence entries (see Vulnerability). Only when that index itself could not be checked does the response fall back further, to "advisoryCoverageUnavailable": true with totalVulnerabilities: null -- distinct from a real 0, which means the check ran and found nothing.
Response Fields
Top level (data)
| Field | Type | Description |
|---|---|---|
package | string | The name as requested |
normalizedName | string | The name used for the version lookup and the OSV query, with any package-set prefix removed (python3Packages becomes requests) |
nixpkgsVersions | object | Versions keyed by channel: nixpkgs-unstable and the current and previous NixOS releases. Empty when no version source answered |
newestVersion | string|null | Newest version the version source reports. From Repology this spans every repository it tracks, nixpkgs or not, so it can be ahead of every nixpkgs channel. Absent when no version source answered |
versionSource | string|null | repology or nixos-search, whichever supplied nixpkgsVersions. null when neither did |
degraded | boolean | Present, and true, only when versionSource is null. See Degraded responses |
vulnerabilities | array | One entry per OSV advisory for the per-ecosystem path; for the ecosystemFallback path, one entry per deduplicated CVE ID found (an advisory with more than one CVE alias yields more than one entry; the same CVE found on more than one advisory yields one), plus one entry for a matched advisory with no CVE alias at all, keyed by its own id. Sorted by confidence descending |
totalVulnerabilities | number|null | Length of vulnerabilities. null, never a fabricated 0, when advisoryCoverageUnavailable is true |
highConfidenceCount | number|null | Entries whose confidence is high. null under the same condition as totalVulnerabilities |
ecosystemsQueried | string[] | The OSV ecosystems this request actually asked, from What is queried. Empty for a name matching no naming segment (most C/C++ packages) -- see ecosystemFallback for what covers that case instead |
ecosystemFallback | boolean | Present, and true, only when ecosystemsQueried is empty and the broader advisory-index lookup described in What is queried ran and answered. Absent for the ordinary per-ecosystem path, and absent (in favor of advisoryCoverageUnavailable) when that lookup itself could not be checked |
advisoryCoverageUnavailable | boolean | Present, and true, only when ecosystemsQueried is empty and the advisory-index fallback could not be read either (unbound, a failed query, or a failed upstream-CVE join). vulnerabilities is [], and totalVulnerabilities/highConfidenceCount are null -- this is "could not check," never cached, and never the same as a real zero |
truncated | boolean | true when OSV held more pages for an ecosystem than this endpoint's bound reads (4 pages, or a byte budget of 12 MB divided evenly across every ecosystem queried), or the advisory-index fallback had more rows than its own one-page read |
truncatedEcosystems | string[] | Always present; empty unless truncated is true. When non-empty: which ecosystems hit the bound, or ["search-index"] for the fallback path |
Channel version (nixpkgsVersions.<channel>)
| Field | Type | Description |
|---|---|---|
version | string|null | Version in that channel; null when identityUnresolved is true |
status | string|null | Repology's status for it: newest, outdated, devel and so on; null when identityUnresolved is true |
vulnerable | boolean|null | Repology's own vulnerable flag for that exact version in that exact repository, when Repology sent one. null whenever Repology sent no flag for that row (most rows) or identityUnresolved is true -- never a fabricated false, which is not an assertion that the version is safe |
identityUnresolved | boolean | Present only when true: Repology named more than one candidate package for this name in this channel with no way to tell them apart, so version/status/vulnerable are withheld rather than guessed |
ambiguousPackages | number | Present whenever Repology named more than one candidate, whether or not identity resolved -- a resolved channel can still carry this if the match it picked had siblings |
Vulnerability
| Field | Type | Description |
|---|---|---|
osvId | string | OSV identifier (GHSA-, PYSEC-, GO-), or the advisory index's own row id for an ecosystemFallback entry (which may not itself be a CVE or an OSV id, e.g. a distro advisory id) |
cveId | string | The CVE alias when the record carries one, otherwise the identifier above |
source | string | Always osv |
lookupSource | string | Present, and 'search-index', only on an ecosystemFallback entry: which retrieval path found it, distinct from source (which records where the data itself came from) |
ecosystem | string|undefined | OSV ecosystem the advisory was found under (npm, PyPI, Go, ...). Absent on an ecosystemFallback entry, which has no single OSV ecosystem |
confidence | object | { score, factors, level }; see Confidence. An ecosystemFallback entry is always { score: 0, factors: [], level: 'low' } -- a name match only, never a fabricated version-range verdict |
matchBasis | string | Present, and 'name-match-only', only on an ecosystemFallback entry |
limitations | string[]|undefined | Present only on an ecosystemFallback entry: why its confidence is capped (no OSV affected-range data available from that lookup path) |
matches | array|undefined | One { product, ecosystem, ranges, cveId } entry per affected package in the record whose name matches the query. Absent on an ecosystemFallback entry |
summary | string|undefined | Advisory summary, when the record carries one |
severity | array|undefined | OSV severity entries, when present. Absent on an ecosystemFallback entry |
references | array|undefined | OSV reference objects, when present. Absent on an ecosystemFallback entry |
What is queried
The endpoint makes two lookups and joins them.
- Versions. Repology (or the service's copy of it) is asked for the project; if it has no nixpkgs entry, the NixOS package search is tried, using the last segment of a dotted name. The result fills
nixpkgsVersions. - Advisories. OSV is queried for
normalizedNamein a set of ecosystems chosen from the requested name, by a nixpkgs naming segment, not a bare substring:PyPIfor apythonsegment or apyprefix,npmfor anodeornpmsegment,RubyGemsforrubyorgem,Goforgo,crates.ioforrustorcargo-- sopython3Packages.djangois queried inPyPI, butdjangoalone matches no segment and queries nothing. A name matching no segment is queried in no ecosystem at all -- seeecosystemsQueriedbelow for which ecosystems a given response actually asked.
No distribution ecosystem (Debian, Ubuntu, Alpine and so on) is queried by this per-ecosystem path, and the nixpkgs security issues are not consulted. An empty vulnerabilities array with a non-empty ecosystemsQueried means OSV held nothing under that name in the ecosystems actually asked; it is not evidence that the nixpkgs package is free of vulnerabilities.
- Advisory-index fallback, for a name matching no naming segment. A name matching no segment above additionally queries the D1 advisory index the weekly digest also relies on -- built from OSV's advisory dump across every package, independent of the naming-segment heuristic. A match there sets
ecosystemFallback: trueand produces a Vulnerability entry withconfidence.level: 'low'and no version-range verdict: the index stores a flattened summary, not OSV's fullaffectedstructure, so this path cannot compare a version the way the per-ecosystem path can. When that index itself cannot be read, the response instead reportsadvisoryCoverageUnavailable: true-- "could not check," never cached, and never the same as a real zero. Still no distribution ecosystem is queried directly by either path: their OSV records carry non-standard ids and distro-specific version schemes this endpoint does not attempt to compare against nixpkgs.
For CVE coverage of a C library beyond what the above finds, use CVE Lookup, Nix CVEs or the nixpkgs security issues.
Confidence
The scoring rules are:
| Factor | Points | Condition |
|---|---|---|
exact_name_match | 40 | An affected package in the record has exactly normalizedName |
partial_name_match | 20 | One name contains the other |
version_in_vulnerable_range | 30 | The nixpkgs-unstable version falls before a range's fixed event |
version_fixed | 10 | The nixpkgs-unstable version is at or past a range's fixed event |
critical_severity | 10 | CVSS 9.0 or above, or OSV severity CRITICAL |
high_severity | 5 | CVSS 7.0 or above, or OSV severity HIGH |
Each factor counts once at its strongest, not once per matching entry in the record, so the maximum score is 80 (exact_name_match + one of the version/severity factors) in practice; 100 would need every factor to fire at once. level is high at 70 or above, medium at 40 or above, and low below that.
Severity is read from the record's own CVSS v3 vector when one parses; when none does, it falls back to a GitHub-style severity word (database_specific.severity) if the record carries one. Version ranges are read as ordered introduced/fixed intervals per matching affected entry; last_affected is treated as inclusive, and an interval with no fixed event counts as still vulnerable. The full record for any entry is available from OSV Details by its osvId.
Degraded responses
When neither Repology nor the NixOS package search returns versions for the name, the response carries versionSource: null and degraded: true, nixpkgsVersions is empty and newestVersion is absent. OSV is still queried, so vulnerabilities can be populated. A live response for golang.org/x/crypto, with vulnerabilities truncated to its first entry:
{
"success": true,
"data": {
"package": "golang.org/x/crypto",
"normalizedName": "golang.org/x/crypto",
"nixpkgsVersions": {},
"versionSource": null,
"degraded": true,
"truncated": false,
"vulnerabilities": [
{
"matches": [{ "product": "golang.org/x/crypto", "ecosystem": "Go", "ranges": [{ "introduced": "0", "fixed": "0.17.0" }], "cveId": "CVE-2023-48795" }],
"confidence": { "score": 70, "factors": ["exact_name_match", "version_in_vulnerable_range"], "level": "high" },
"cveId": "CVE-2023-48795",
"source": "osv",
"ecosystem": "Go",
"osvId": "GHSA-3vm4-22fp-5rfm",
"summary": "golang.org/x/crypto vulnerable to Terrapin Attack",
"severity": [{ "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N" }]
}
],
"totalVulnerabilities": 58,
"highConfidenceCount": 9
},
"timestamp": "2026-10-15T19:55:05.238Z"
}The same shape is returned for a name nixpkgs does not carry at all and for one whose version lookup failed; the response does not distinguish the two. Degraded responses are served with Cache-Control: no-store and are never cached.
Errors
| Status | Body | Cause |
|---|---|---|
400 | {"success": false, "error": "Invalid package name"} | The name contains a character outside the allowed set |
404 | {"success": false, "error": "Not found | No package segment in the path (/api) |
500 | {"success": false, "error": "Failed to find vulnerabilities"} | OSV did not answer. The endpoint fails rather than return an empty list for a package it could not check |
429 | See Rate Limits | Rate limit exceeded |
Examples
curl "https://api.vulnpatch.dev/api/v1/vulns/openssl"
curl "https://api.vulnpatch.dev/api/v1/vulns/python3Packages.requests"async function listAdvisories(packageName) {
const response = await fetch(
`https://api.vulnpatch.dev/api/v1/vulns/${packageName}`
);
const body = await response.json();
if (!body.success) throw new Error(body.error);
const { data } = body;
if (data.degraded) {
console.warn(`No nixpkgs versions found for ${data.package}`);
} else {
console.log(`nixpkgs-unstable: ${data.nixpkgsVersions['nixpkgs-unstable']?.version}`);
}
for (const v of data.vulnerabilities) {
console.log(`${v.osvId} (${v.ecosystem})`);
}
}import requests
def list_advisories(package_name):
body = requests.get(
f'https://api.vulnpatch.dev/api/v1/vulns/{package_name}'
).json()
if not body['success']:
raise RuntimeError(body['error'])
data = body['data']
if data.get('degraded'):
print(f"No nixpkgs versions found for {data['package']}")
else:
unstable = data['nixpkgsVersions'].get('nixpkgs-unstable', {})
print(f"nixpkgs-unstable: {unstable.get('version')}")
for v in data['vulnerabilities']:
print(f"{v['osvId']} ({v['ecosystem']})")Differences from /osv
| Aspect | /osv | /vulns |
|---|---|---|
| Ecosystems queried | All OSV ecosystems, or one via ?ecosystem | Chosen from the name; the search-index fallback covers a name matching none of them |
| nixpkgs versions | No | Yes, per channel |
| Per-advisory confidence | No | Yes, scored from the matched affected entry's name, version range and severity |
| Name normalization | No | Package-set prefixes stripped |
Caching
Successful, non-degraded responses are cached at the edge for two hours, keyed on the full URL plus an internal cache-format version. X-Cache reads HIT or MISS; a hit also carries X-Cache-Time, and its timestamp is the time the cached response was built, not the time of the request. A response with advisoryCoverageUnavailable: true is sent with Cache-Control: no-store and is never cached.