Vulnerability Matching
How GET /api/v1/vulns/:package relates a nixpkgs package name to OSV advisories, and what its confidence score does and does not tell you.
The naming problem
Advisory databases and package repositories name the same software differently. nixpkgs groups language packages into sets (python3Packages.requests), versions some attribute names (libsoup_3), and ships variants of one project under several attributes. OSV files advisories under the name each language ecosystem uses (requests on PyPI). A lookup has to translate between them, and every translation can be wrong.
How a name is resolved
Package-set prefixes are stripped
A known package-set prefix is removed before any lookup. The result is returned as normalizedName:
python3Packages.requests -> requests
nodePackages.foo -> fooThe prefixes covered are the Python, Node.js, Perl, Ruby, Haskell, Lua, WordPress, OCaml, PHP, R, Emacs, Vim, Linux kernel module, BEAM, KDE, GNOME, Cinnamon, XFCE, TeX, Rust, Go, Coq and Agda package sets.
Versions come from Repology, then the NixOS package search
The stripped name is looked up in Repology (or the service's copy of it). A versioned attribute with no Repology project of its own is retried without its suffix (libsoup_3 becomes libsoup). If Repology has no nixpkgs entry, the NixOS package search is tried with the last segment of a dotted name. The versions found fill nixpkgsVersions, and versionSource names the source that supplied them.
When neither source returns versions the response is marked degraded: true with versionSource: null. It does not say whether the package is absent from nixpkgs or the lookup failed. See Degraded responses.
Advisories come from OSV, in ecosystems guessed from the name
OSV is queried for normalizedName in ecosystems chosen from the requested name:
| Recognized name segment | Ecosystem queried |
|---|---|
python, or starts with py | PyPI |
node or npm | npm |
ruby or gem | RubyGems |
go or golang | Go |
rust or cargo | crates |
| none of the above | no OSV ecosystem at all -- see below |
The naming match is anchored to a segment of the name (the start, or right after a ./-/_ separator, ending at another separator, a digit, a camelCase capital, or the end of the name), not a bare substring: django does not match go, nor do mongodb, argon2, gemini or google-chrome match the segments they happen to contain. Request a language package by its full nixpkgs attribute (python3Packages.django) so the package-set prefix selects the right ecosystem; a bare django matches no segment.
A name matching no segment gets no OSV ecosystem query at all, which ecosystemsQueried: [] in the response discloses -- it is not guessed into npm, PyPI and Go, which would let a C library like curl or openssl pick up an unrelated same-named language package's advisories as its own.
That is not the whole answer for such a name, though: it additionally queries the same D1 advisory index the weekly digest relies on (built from OSV's advisory dump across every package, not gated by this naming heuristic), setting ecosystemFallback: true on a match -- see What is queried for what that entry can and cannot tell you (name match only, no version-range verdict), and advisoryCoverageUnavailable for when even that could not be checked. No distribution ecosystem (Debian, Ubuntu, Alpine) 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.
Confidence scores
Each advisory carries a confidence object, not a single label:
{ "score": 80, "factors": ["exact_name_match", "version_in_vulnerable_range"], "level": "high" }score is the sum of the factors that applied, each counted once at its strongest (not once per matching entry in the record), so 80 is the practical ceiling -- 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.
| 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 |
The version compared is always the one nixpkgs carries in nixpkgs-unstable. There is no way to supply your own version: the endpoint reads no query parameters, and ?version= is ignored.
What the score means
Every factor depends on the affected packages, version ranges and severity in the OSV record, read from OSV's own single-query response for the detected ecosystem (not the abbreviated batch form). Severity is read from the record's CVSS v3 vector when one parses, falling back to a GitHub-style severity word otherwise. Version ranges are read as ordered intervals per matching affected entry, with last_affected treated as inclusive and an interval with no fixed event counted as still vulnerable. vulnerabilities is sorted by confidence.score descending, so the ordering is meaningful. A low or zero score still means what it says: the matched entry's name, range or severity data did not support a higher one. Read the full record from OSV Details if you want to judge the match yourself from its raw affected entries.
API response
A live response for python3Packages.requests, with vulnerabilities truncated to its first entry:
{
"success": true,
"data": {
"package": "python3Packages.requests",
"normalizedName": "requests",
"nixpkgsVersions": {
"nixos-25.11": { "version": null, "status": null, "vulnerable": null, "identityUnresolved": true, "ambiguousPackages": 2 },
"nixos-26.05": { "version": null, "status": null, "vulnerable": null, "identityUnresolved": true, "ambiguousPackages": 2 },
"nixpkgs-unstable": { "version": null, "status": null, "vulnerable": null, "identityUnresolved": true, "ambiguousPackages": 2 }
},
"newestVersion": "2.34.2",
"versionSource": "repology",
"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_fixed"], "level": "high" },
"cveId": "CVE-2024-35195",
"source": "osv",
"ecosystem": "PyPI",
"osvId": "GHSA-652x-xj99-gmcc",
"summary": "Requests vulnerable to session verify=False persisting",
"severity": [{ "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N" }]
}
],
"totalVulnerabilities": 16,
"highConfidenceCount": 6
},
"timestamp": "2026-10-15T19:52:14.024Z"
}identityUnresolved: true means this channel's Repology mirror data names more than one candidate package with no way to tell them apart, so version/status/vulnerable stay unset for it. ambiguousPackages counts the candidates. A channel can carry ambiguousPackages even when it did resolve a version, if a resolvable match still had siblings. Every field is described in CVE Matching.
Narrowing a query
To query one ecosystem by its own package name, use the OSV endpoint with ecosystem, which returns OSV's full records:
curl "https://api.vulnpatch.dev/api/v1/osv/requests?ecosystem=PyPI"False positives and false negatives
Both are expected. The common causes:
- Name segment misfires. A nixpkgs attribute whose naming segment points to the wrong language ecosystem returns that ecosystem's advisories and misses the right one's -- see the segment table above.
- Same name, different distribution. The search-index fallback for a name matching no segment excludes a row tied only to another language's package manager, but a distribution package of the same name as an unrelated nixpkgs package (rare, since most distributions repackage the same upstream project they are named after) could still be attributed to it.
- Missing coverage. The nixpkgs security issues are not consulted by either path. An empty
vulnerabilitieslist, orecosystemFallbackfinding nothing, is not proof that a package is unaffected. - Bundled dependencies. A vulnerability in a library the package vendors is filed under that library's name, not the package's.
Verify any finding you will act on against the full advisory record.