Knowledge Graph API
The knowledge graph was a Neo4j-backed set of relationship queries across CVEs, packages and weaknesses. Its backend is not deployed. Relationship questions are answered by Relations (GET /api/v1/relations/{id}), which reads the search index and the dossier and needs no graph database. This page says exactly what the /graph routes answer today, so that a 503 from one of them is read as "not deployed" and never as "nothing found".
What answers today
One route answers, from OSV rather than a graph:
GET /api/v1/graph/package/:name/vulns?ecosystem=npmEvery other /graph route returns 503 with code: "GRAPH_UNAVAILABLE". The scheduler's graph_sync and graph_nvd_enrichment tasks are skipped on every run for the same reason, so there is no graph data anywhere to be stale.
| Route | Status | Where the same question is answered |
|---|---|---|
GET /graph/package/:name/vulns | Answers, from OSV | OSV Vulnerabilities for every ecosystem at once |
GET /graph/stats | 503 GRAPH_UNAVAILABLE | Statistics |
GET /graph/cve/:id/affected | 503 GRAPH_UNAVAILABLE | Relations affects edges, or canonical.affectedPackages in the CVE dossier |
GET /graph/cve/:id/related | 503 GRAPH_UNAVAILABLE | Relations for the graph around the CVE, Aliases for its other ids |
GET /graph/package/:name/transitive | 503 GRAPH_UNAVAILABLE | Not answered anywhere; SBOM Analysis scans a dependency list you supply |
GET /graph/cwe/:id/cves | 503 GRAPH_UNAVAILABLE | canonical.cwes in the dossier gives the weaknesses of one CVE; the reverse lookup is not answered |
GET /graph/packages/top, /graph/packages/search, /graph/sample | 503 GRAPH_UNAVAILABLE | Search |
The 503 body
{
"success": false,
"error": "The knowledge graph backend is not deployed, so this route cannot answer.",
"code": "GRAPH_UNAVAILABLE",
"hint": "Not a statement about the CVE, package or weakness asked for. ...",
"links": {
"relations": "https://api.vulnpatch.dev/api/v1/relations/{id}",
"dossier": "https://api.vulnpatch.dev/api/v1/cve/{id}/dossier",
"osv": "https://api.vulnpatch.dev/api/v1/osv/{package}"
}
}The response carries Cache-Control: no-store. A client that receives it has learnt nothing about the identifier it asked after.
Get CVEs for Package
Returns the vulnerabilities OSV.dev lists for a package in one ecosystem. The path keeps its /graph prefix because the package pages on cve.vulnpatch.dev call it; the answer comes from OSV, and the body says so in source.
GET /api/v1/graph/package/:name/vulns?ecosystem=:ecosystemParameters
| Parameter | In | Required | Description |
|---|---|---|---|
name | path | Yes | Package name as the ecosystem spells it (e.g. lodash) |
ecosystem | query | Yes | OSV ecosystem (npm, PyPI, Go, crates.io, Debian, ...). OSV rejects a query without one, so the route answers 400 rather than sending it. |
Response
{
"success": true,
"data": {
"package": "lodash",
"ecosystem": "npm",
"source": "osv",
"vulnerabilities": [
{
"cveId": "CVE-2021-23337",
"advisoryId": "GHSA-35jh-r3h4-6jhm",
"cveIds": ["CVE-2021-23337", "CVE-2026-4800"],
"upstream": [],
"severity": "high",
"score": 7.2,
"published": "2021-05-06T16:05:51Z",
"description": "Command Injection in lodash"
}
],
"count": 1
},
"timestamp": "2026-09-28T00:12:41.020Z"
}One row per OSV record. cveId is the CVE where OSV assigned one and the record's id otherwise; advisoryId is always the record's id, and cveIds lists every CVE it names. A record is left out only when every CVE it names is already on an earlier row, so a GHSA and a PYSEC for one CVE are one row, but an advisory that names a second CVE is kept. A downstream rebuild, such as Root's ROOT-OS-ALPINE-320-CVE-2024-47611, names its CVE only in upstream: it is not the CVE record, and cveId is its own id.
| Field | Description |
|---|---|
cveId | The CVE id where OSV has assigned one, otherwise the advisory id (GHSA-...) |
severity | low, medium, high, critical or unknown, from the OSV severity block |
score | CVSS base score where OSV carries one, otherwise null |
published | OSV publication time |
timestamp | When this answer was produced. OSV is queried live; nothing is cached on this route |
Rows are ordered most recently published first. Fixed versions are not in this shape; the OSV Details route carries the affected ranges for one advisory.
Failures
| Status | code | Meaning |
|---|---|---|
400 | ECOSYSTEM_REQUIRED | No ecosystem given. links.osv points at /osv/:package, which asks every ecosystem |
500 | SOURCE_UNAVAILABLE | OSV did not answer. Not a statement that the package is clean; retry |
429 | Rate limited, as on every route |
An empty vulnerabilities list with success: true means OSV lists nothing for that package in that ecosystem. A failure is never rendered as an empty list.
Where this stands
The graph queries were a visualisation experiment. The Neo4j instance behind them stopped being maintained and began failing, and the one route visitors reached, the package lookup, was moved to OSV so that package pages stopped reporting an outage as "no known vulnerabilities". The code for the remaining routes and for the scheduler tasks is still in the worker and would answer again if NEO4J_URI and NEO4J_PASSWORD were bound, but nothing populates that graph and its contents would start empty.
Relationship questions that the graph promised, and where they are answered now:
- The graph around an identifier: Relations, read from the search index and the dossier, with the source of every edge
- What does this CVE affect:
canonical.affectedPackagesin the dossier, from the CNA and NVD records - What weakness is this:
canonical.cwesin the dossier, with the source of each - Other ids for the same advisory: Aliases
- Transitive dependency risk: not answered. SBOM Analysis scans a dependency list you already hold